Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public class BlobSasImplUtil {
*/
private static final String SAS_CONTAINER_CONSTANT = "c";

/**
* The SAS blob directory constant.
*/
private static final String SAS_BLOB_DIRECTORY_CONSTANT = "d";

private static final ClientLogger LOGGER = new ClientLogger(BlobSasImplUtil.class);

private static final String VERSION = Configuration.getGlobalConfiguration()
Expand Down Expand Up @@ -81,6 +86,8 @@ public class BlobSasImplUtil {
private String delegatedUserObjectId;
private Map<String, String> requestHeaders;
private Map<String, String> requestQueryParameters;
private Boolean isDirectory;
private Integer directoryDepth;

/**
* Creates a new {@link BlobSasImplUtil} with the specified parameters
Expand Down Expand Up @@ -130,6 +137,7 @@ public BlobSasImplUtil(BlobServiceSasSignatureValues sasValues, String container
this.delegatedUserObjectId = sasValues.getDelegatedUserObjectId();
this.requestHeaders = sasValues.getRequestHeaders();
this.requestQueryParameters = sasValues.getRequestQueryParameters();
this.isDirectory = sasValues.isDirectory();
}

/**
Expand Down Expand Up @@ -256,6 +264,11 @@ private String encode(UserDelegationKey userDelegationKey, String signature) {
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_DELEGATED_USER_OBJECT_ID,
this.delegatedUserObjectId);
}

if (this.isDirectory != null && this.isDirectory) {
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_DIRECTORY_DEPTH, this.directoryDepth);
}

tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_RESOURCE, this.resource);
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_PERMISSIONS, this.permissions);
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNATURE, signature);
Expand All @@ -282,7 +295,8 @@ private String encode(UserDelegationKey userDelegationKey, String signature) {
* a. If "BlobName" is _not_ set, it is a container resource.
* b. Otherwise, if "SnapshotId" is set, it is a blob snapshot resource.
* c. Otherwise, if "VersionId" is set, it is a blob version resource.
* d. Otherwise, it is a blob resource.
* d. Otherwise, if "IsDirectory" is set to true, it is a blob directory resource.
* e. Otherwise, it is a blob resource.
* 4. Reparse permissions depending on what the resource is. If it is an unrecognized resource, do nothing. </p>
*
* Taken from:
Expand All @@ -303,6 +317,8 @@ public void ensureState() {
resource = SAS_BLOB_SNAPSHOT_CONSTANT;
} else if (versionId != null) {
resource = SAS_BLOB_VERSION_CONSTANT;
} else if (isDirectory != null && isDirectory) {
resource = SAS_BLOB_DIRECTORY_CONSTANT;
} else {
resource = SAS_BLOB_CONSTANT;
}
Expand All @@ -319,6 +335,17 @@ public void ensureState() {
permissions = BlobContainerSasPermission.parse(permissions).toString();
break;

case SAS_BLOB_DIRECTORY_CONSTANT:
// Normalize backslashes to forward slashes to align directory depth with canonical name computation.
String normalizedBlobName = blobName.replace('\\', '/');
if (!normalizedBlobName.equalsIgnoreCase("/")) {
directoryDepth = normalizedBlobName.trim().replaceAll("^/+|/+$", "").split("/").length;
} else {
directoryDepth = 0;
}
permissions = BlobSasPermission.parse(permissions).toString();
break;

default:
// We won't reparse the permissions if we don't know the type.
LOGGER.info("Not re-parsing permissions. Resource type '{}' is unknown.", resource);
Expand Down Expand Up @@ -507,4 +534,8 @@ public String getResource() {
public String getPermissions() {
return this.permissions;
}

public Integer getDirectoryDepth() {
return directoryDepth;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ public final class BlobServiceSasSignatureValues {
private String delegatedUserObjectId;
private Map<String, String> requestHeaders;
private Map<String, String> requestQueryParameters;
private Boolean isDirectory;

/**
* Creates an object with empty values for all fields.
Expand Down Expand Up @@ -655,6 +656,27 @@ public BlobServiceSasSignatureValues setRequestQueryParameters(Map<String, Strin
return this;
}

/**
* Gets whether the {@code blobName} is a virtual directory. Required when the {@code resource} is set to "d".
*
* @return Whether the {@code blobName} is a virtual directory. Required when the resource is set to "d".
*/
public Boolean isDirectory() {
return isDirectory;
}

/**
* Beginning in version 2020-02-10, this value defines whether the {@code blobName} is a virtual directory.
* Required when the {@code resource} is set to "d".
*
* @param isDirectory Whether the {@code blobName} is a virtual directory. Required when the resource is set to "d".
* @return the updated BlobServiceSasSignatureValues object
*/
public BlobServiceSasSignatureValues setDirectory(Boolean isDirectory) {
this.isDirectory = isDirectory;
return this;
}

/**
* Uses an account's shared key credential to sign these signature values to produce the proper SAS query
* parameters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,29 @@
import com.azure.storage.blob.sas.BlobContainerSasPermission;
import com.azure.storage.blob.sas.BlobSasPermission;
import com.azure.storage.blob.sas.BlobServiceSasSignatureValues;
import com.azure.storage.common.implementation.Constants;
import com.azure.storage.common.implementation.StorageImplUtils;
import com.azure.storage.common.sas.CommonSasQueryParameters;
import com.azure.storage.common.sas.SasIpRange;
import com.azure.storage.common.sas.SasProtocol;
import com.azure.storage.common.test.shared.extensions.RequiredServiceVersion;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -188,29 +201,197 @@ public void ensureStateIllegalArgument() {
@ParameterizedTest
@MethodSource("ensureStateResourceAndPermissionSupplier")
public void ensureStateResourceAndPermission(String container, String blob, String snapshot, String versionId,
BlobContainerSasPermission blobContainerSasPermission, BlobSasPermission blobSasPermission, String resource,
String permissionString) {
boolean isDirectory, Integer directoryDepth, BlobContainerSasPermission blobContainerSasPermission,
BlobSasPermission blobSasPermission, String resource, String permissionString) {
OffsetDateTime expiryTime = testResourceNamer.now().plusDays(1);

BlobServiceSasSignatureValues values = blobContainerSasPermission != null
? new BlobServiceSasSignatureValues(expiryTime, blobContainerSasPermission)
: new BlobServiceSasSignatureValues(expiryTime, blobSasPermission);
? new BlobServiceSasSignatureValues(expiryTime, blobContainerSasPermission).setDirectory(isDirectory)
: new BlobServiceSasSignatureValues(expiryTime, blobSasPermission).setDirectory(isDirectory);

BlobSasImplUtil implUtil = new BlobSasImplUtil(values, container, blob, snapshot, versionId, null);
implUtil.ensureState();

assertEquals(resource, implUtil.getResource());
assertEquals(permissionString, implUtil.getPermissions());
assertEquals(directoryDepth, implUtil.getDirectoryDepth());
}

private static Stream<Arguments> ensureStateResourceAndPermissionSupplier() {
return Stream.of(
Arguments.of("container", null, null, null,
// container, blob, snapshot, versionId, isDirectory, directoryDepth, containerSasPermission,
// blobSasPermission, resource, permissionString
Arguments.of("container", null, null, null, false, null,
new BlobContainerSasPermission().setReadPermission(true).setListPermission(true), null, "c", "rl"),
Arguments.of("container", "blob", null, null, null, new BlobSasPermission().setReadPermission(true), "b",
"r"),
Arguments.of("container", "blob", "snapshot", null, null, new BlobSasPermission().setReadPermission(true),
"bs", "r"),
Arguments.of("container", "blob", null, "version", null, new BlobSasPermission().setReadPermission(true),
"bv", "r"));
Arguments.of("container", "blob", null, null, false, null, null,
new BlobSasPermission().setReadPermission(true), "b", "r"),
Arguments.of("container", "blob", "snapshot", null, false, null, null,
new BlobSasPermission().setReadPermission(true), "bs", "r"),
Arguments.of("container", "blob", null, "version", false, null, null,
new BlobSasPermission().setReadPermission(true), "bv", "r"),
Arguments.of("container", "foo/bar/hello", null, null, true, 3, null,
new BlobSasPermission().setReadPermission(true), "d", "r"),
Arguments.of("container", "foo/bar", null, null, true, 2, null,
new BlobSasPermission().setReadPermission(true), "d", "r"),
Arguments.of("container", "foo/", null, null, true, 1, null,
new BlobSasPermission().setReadPermission(true), "d", "r"),
Arguments.of("container", "/", null, null, true, 0, null, new BlobSasPermission().setReadPermission(true),
"d", "r"));
}

/**
* Validates encoded query parameters for a directory scoped blob SAS signed with the account key.
*/
@RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
@Test
public void toSasQueryParametersDirectoryTest() {
String containerName = generateContainerName();
String blobName = "foo/bar/hello";

OffsetDateTime start = OffsetDateTime.of(2020, 1, 2, 3, 4, 5, 0, ZoneOffset.UTC);
OffsetDateTime expiry = OffsetDateTime.of(2020, 1, 3, 3, 4, 5, 0, ZoneOffset.UTC);
SasIpRange ipRange = new SasIpRange().setIpMin("1.1.1.1").setIpMax("2.2.2.2");

BlobSasPermission permissions = getAllBlobSasPermissions();

BlobServiceSasSignatureValues sasValues
= new BlobServiceSasSignatureValues(expiry, permissions).setDirectory(true)
.setIdentifier("myidentifier")
.setStartTime(start)
.setProtocol(SasProtocol.HTTPS_HTTP)
.setSasIpRange(ipRange)
.setCacheControl("cache")
.setContentDisposition("disposition")
.setContentEncoding("encoding")
.setContentLanguage("language")
.setContentType("type");

BlobSasImplUtil implUtil = new BlobSasImplUtil(sasValues, containerName, blobName, null, null, null);

List<String> stringToSignHolder = new ArrayList<>();
String sasToken = implUtil.generateSas(ENVIRONMENT.getPrimaryAccount().getCredential(), stringToSignHolder::add,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good use of the stringToSignHolderHandler. I remember seeing this when I was debugging some tests a while back and it was quite useful for seeing the actual values that go in instead of a bunch of trying to count newlines 😅

Context.NONE);
assertEquals(1, stringToSignHolder.size());
assertNotNull(stringToSignHolder.get(0));

CommonSasQueryParameters qp
= BlobUrlParts.parse("https://account.blob.core.windows.net/c?" + sasToken).getCommonSasQueryParameters();

String expectedSig = ENVIRONMENT.getPrimaryAccount().getCredential().computeHmac256(stringToSignHolder.get(0));

assertEquals(Constants.SAS_SERVICE_VERSION, qp.getVersion());
assertNull(qp.getServices());
assertNull(qp.getResourceTypes());
assertEquals(SasProtocol.HTTPS_HTTP, qp.getProtocol());
assertEquals(start, qp.getStartTime());
assertEquals(expiry, qp.getExpiryTime());
assertEquals(ipRange.getIpMin(), qp.getSasIpRange().getIpMin());
assertEquals(ipRange.getIpMax(), qp.getSasIpRange().getIpMax());
assertEquals("myidentifier", qp.getIdentifier());
assertEquals("d", qp.getResource());
assertEquals(3, qp.getDirectoryDepth());
assertEquals(permissions.toString(), qp.getPermissions());
assertEquals(expectedSig, qp.getSignature());
assertEquals("cache", qp.getCacheControl());
assertEquals("disposition", qp.getContentDisposition());
assertEquals("encoding", qp.getContentEncoding());
assertEquals("language", qp.getContentLanguage());
assertEquals("type", qp.getContentType());
}

/**
* Validates encoded query parameters for a directory scoped user delegation SAS,
* including delegated OID and request header/query key lists.
*/
@RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2020-02-10")
@Test
public void toSasQueryParametersDirectoryIdentityTest() {
String containerName = generateContainerName();
String blobName = "foo/bar/hello";
String accountName = ENVIRONMENT.getPrimaryAccount().getName();

OffsetDateTime start = OffsetDateTime.of(2020, 1, 2, 3, 4, 5, 0, ZoneOffset.UTC);
OffsetDateTime expiry = OffsetDateTime.of(2020, 1, 3, 3, 4, 5, 0, ZoneOffset.UTC);
OffsetDateTime keyStart = OffsetDateTime.of(2020, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC);
OffsetDateTime keyExpiry = OffsetDateTime.of(2020, 1, 10, 0, 0, 0, 0, ZoneOffset.UTC);

SasIpRange ipRange = new SasIpRange().setIpMin("1.1.1.1").setIpMax("2.2.2.2");

BlobSasPermission permissions = getAllBlobSasPermissions();

Map<String, String> requestHeaders = new TreeMap<>();
requestHeaders.put("a-header", "a-value");
requestHeaders.put("b-header", "b-value");

Map<String, String> requestQueryParams = new TreeMap<>();
requestQueryParams.put("q-one", "1");
requestQueryParams.put("q-two", "2");

String delegatedOid = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
String delegatedKeyTid = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb";

BlobServiceSasSignatureValues sasValues
= new BlobServiceSasSignatureValues(expiry, permissions).setDirectory(true)
.setStartTime(start)
.setProtocol(SasProtocol.HTTPS_HTTP)
.setSasIpRange(ipRange)
.setDelegatedUserObjectId(delegatedOid)
.setRequestHeaders(requestHeaders)
.setRequestQueryParameters(requestQueryParams)
.setCacheControl("cache")
.setContentDisposition("disposition")
.setContentEncoding("encoding")
.setContentLanguage("language")
.setContentType("type");

UserDelegationKey key = new UserDelegationKey().setSignedObjectId("keyOid")
.setSignedTenantId("keyTid")
.setSignedStart(keyStart)
.setSignedExpiry(keyExpiry)
.setSignedService("b")
.setSignedVersion("2019-02-02")
.setSignedDelegatedUserTenantId(delegatedKeyTid)
.setValue(ENVIRONMENT.getPrimaryAccount().getKey());

BlobSasImplUtil implUtil = new BlobSasImplUtil(sasValues, containerName, blobName, null, null, null);

List<String> stringToSignHolder = new ArrayList<>();
String sasToken = implUtil.generateUserDelegationSas(key, accountName, stringToSignHolder::add, Context.NONE);
assertEquals(1, stringToSignHolder.size());
assertNotNull(stringToSignHolder.get(0));

CommonSasQueryParameters qp
= BlobUrlParts.parse("https://account.blob.core.windows.net/c?" + sasToken).getCommonSasQueryParameters();

String expectedSig = StorageImplUtils.computeHMac256(key.getValue(), stringToSignHolder.get(0));

assertEquals(Constants.SAS_SERVICE_VERSION, qp.getVersion());
assertNull(qp.getServices());
assertNull(qp.getResourceTypes());
assertEquals(SasProtocol.HTTPS_HTTP, qp.getProtocol());
assertEquals(start, qp.getStartTime());
assertEquals(expiry, qp.getExpiryTime());
assertEquals(ipRange.getIpMin(), qp.getSasIpRange().getIpMin());
assertEquals(ipRange.getIpMax(), qp.getSasIpRange().getIpMax());
assertNull(qp.getIdentifier());
assertEquals("keyOid", qp.getKeyObjectId());
assertEquals("keyTid", qp.getKeyTenantId());
assertEquals(keyStart, qp.getKeyStart());
assertEquals(keyExpiry, qp.getKeyExpiry());
assertEquals("b", qp.getKeyService());
assertEquals("2019-02-02", qp.getKeyVersion());
assertEquals(delegatedKeyTid, qp.getKeyDelegatedUserTenantId());
assertEquals("d", qp.getResource());
assertEquals(3, qp.getDirectoryDepth());
assertEquals(permissions.toString(), qp.getPermissions());
assertEquals(delegatedOid, qp.getDelegatedUserObjectId());
assertEquals(Arrays.asList("a-header", "b-header"), qp.getRequestHeaders());
assertEquals(Arrays.asList("q-one", "q-two"), qp.getRequestQueryParameters());
assertEquals(expectedSig, qp.getSignature());
assertEquals("cache", qp.getCacheControl());
assertEquals("disposition", qp.getContentDisposition());
assertEquals("encoding", qp.getContentEncoding());
assertEquals("language", qp.getContentLanguage());
assertEquals("type", qp.getContentType());
}
}
Loading
Loading