diff --git a/sdk/storage/azure-storage-queue/assets.json b/sdk/storage/azure-storage-queue/assets.json
index 707c584fd5e6..e5a47f0ac06e 100644
--- a/sdk/storage/azure-storage-queue/assets.json
+++ b/sdk/storage/azure-storage-queue/assets.json
@@ -2,5 +2,5 @@
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "java",
"TagPrefix": "java/storage/azure-storage-queue",
- "Tag": "java/storage/azure-storage-queue_06da25b415"
+ "Tag": "java/storage/azure-storage-queue_2e02d46abe"
}
\ No newline at end of file
diff --git a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueAsyncClient.java b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueAsyncClient.java
index 6df171fedff7..57ef891385dd 100644
--- a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueAsyncClient.java
+++ b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueAsyncClient.java
@@ -37,6 +37,7 @@
import com.azure.storage.queue.models.QueueStorageException;
import com.azure.storage.queue.models.SendMessageResult;
import com.azure.storage.queue.models.UpdateMessageResult;
+import com.azure.storage.queue.models.UserDelegationKey;
import com.azure.storage.queue.sas.QueueServiceSasSignatureValues;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -1516,4 +1517,36 @@ public String generateSas(QueueServiceSasSignatureValues queueServiceSasSignatur
return new QueueSasImplUtil(queueServiceSasSignatureValues, getQueueName())
.generateSas(SasImplUtils.extractSharedKeyCredential(getHttpPipeline()), stringToSignHandler, context);
}
+
+ /**
+ * Generates a user delegation SAS for the queue using the specified {@link QueueServiceSasSignatureValues}.
+ *
See {@link QueueServiceSasSignatureValues} for more information on how to construct a user delegation SAS.
+ *
+ * @param queueServiceSasSignatureValues {@link QueueServiceSasSignatureValues}
+ * @param userDelegationKey A {@link UserDelegationKey} object used to sign the SAS values.
+ *
+ * @return A {@code String} representing the SAS query parameters.
+ */
+ public String generateUserDelegationSas(QueueServiceSasSignatureValues queueServiceSasSignatureValues,
+ UserDelegationKey userDelegationKey) {
+ return generateUserDelegationSas(queueServiceSasSignatureValues, userDelegationKey, null, Context.NONE);
+ }
+
+ /**
+ * Generates a user delegation SAS for the queue using the specified {@link QueueServiceSasSignatureValues}.
+ * See {@link QueueServiceSasSignatureValues} for more information on how to construct a user delegation SAS.
+ *
+ * @param queueServiceSasSignatureValues {@link QueueServiceSasSignatureValues}
+ * @param userDelegationKey A {@link UserDelegationKey} object used to sign the SAS values.
+ * @param stringToSignHandler For debugging purposes only. Returns the string to sign that was used to generate the
+ * signature.
+ * @param context Additional context that is passed through the code when generating a SAS.
+ *
+ * @return A {@code String} representing the SAS query parameters.
+ */
+ public String generateUserDelegationSas(QueueServiceSasSignatureValues queueServiceSasSignatureValues,
+ UserDelegationKey userDelegationKey, Consumer stringToSignHandler, Context context) {
+ return new QueueSasImplUtil(queueServiceSasSignatureValues, getQueueName())
+ .generateUserDelegationSas(userDelegationKey, accountName, stringToSignHandler, context);
+ }
}
diff --git a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueClient.java b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueClient.java
index 85d1994b6412..6bd3751a4328 100644
--- a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueClient.java
+++ b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueClient.java
@@ -32,6 +32,7 @@
import com.azure.storage.queue.implementation.models.QueuesGetAccessPolicyHeaders;
import com.azure.storage.queue.implementation.models.QueuesGetPropertiesHeaders;
import com.azure.storage.queue.implementation.models.SendMessageResultWrapper;
+import com.azure.storage.queue.models.UserDelegationKey;
import com.azure.storage.queue.implementation.util.ModelHelper;
import com.azure.storage.queue.implementation.util.QueueSasImplUtil;
import com.azure.storage.queue.models.PeekedMessageItem;
@@ -1459,4 +1460,36 @@ public String generateSas(QueueServiceSasSignatureValues queueServiceSasSignatur
return new QueueSasImplUtil(queueServiceSasSignatureValues, getQueueName())
.generateSas(SasImplUtils.extractSharedKeyCredential(getHttpPipeline()), stringToSignHandler, context);
}
+
+ /**
+ * Generates a user delegation SAS for the queue using the specified {@link QueueServiceSasSignatureValues}.
+ * See {@link QueueServiceSasSignatureValues} for more information on how to construct a user delegation SAS.
+ *
+ * @param queueServiceSasSignatureValues {@link QueueServiceSasSignatureValues}
+ * @param userDelegationKey A {@link UserDelegationKey} object used to sign the SAS values.
+ *
+ * @return A {@code String} representing the SAS query parameters.
+ */
+ public String generateUserDelegationSas(QueueServiceSasSignatureValues queueServiceSasSignatureValues,
+ UserDelegationKey userDelegationKey) {
+ return generateUserDelegationSas(queueServiceSasSignatureValues, userDelegationKey, null, Context.NONE);
+ }
+
+ /**
+ * Generates a user delegation SAS for the queue using the specified {@link QueueServiceSasSignatureValues}.
+ * See {@link QueueServiceSasSignatureValues} for more information on how to construct a user delegation SAS.
+ *
+ * @param queueServiceSasSignatureValues {@link QueueServiceSasSignatureValues}
+ * @param userDelegationKey A {@link UserDelegationKey} object used to sign the SAS values.
+ * @param stringToSignHandler For debugging purposes only. Returns the string to sign that was used to generate the
+ * signature.
+ * @param context Additional context that is passed through the code when generating a SAS.
+ *
+ * @return A {@code String} representing the SAS query parameters.
+ */
+ public String generateUserDelegationSas(QueueServiceSasSignatureValues queueServiceSasSignatureValues,
+ UserDelegationKey userDelegationKey, Consumer stringToSignHandler, Context context) {
+ return new QueueSasImplUtil(queueServiceSasSignatureValues, getQueueName())
+ .generateUserDelegationSas(userDelegationKey, accountName, stringToSignHandler, context);
+ }
}
diff --git a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueServiceAsyncClient.java b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueServiceAsyncClient.java
index ce58fb71758f..92979bebaf0d 100644
--- a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueServiceAsyncClient.java
+++ b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueServiceAsyncClient.java
@@ -5,6 +5,7 @@
import com.azure.core.annotation.ReturnType;
import com.azure.core.annotation.ServiceClient;
import com.azure.core.annotation.ServiceMethod;
+import com.azure.core.credential.TokenCredential;
import com.azure.core.http.HttpPipeline;
import com.azure.core.http.rest.PagedFlux;
import com.azure.core.http.rest.PagedResponse;
@@ -15,10 +16,12 @@
import com.azure.core.util.logging.ClientLogger;
import com.azure.storage.common.StorageSharedKeyCredential;
import com.azure.storage.common.implementation.AccountSasImplUtil;
+import com.azure.storage.common.implementation.Constants;
import com.azure.storage.common.implementation.SasImplUtils;
import com.azure.storage.common.implementation.StorageImplUtils;
import com.azure.storage.common.sas.AccountSasSignatureValues;
import com.azure.storage.queue.implementation.AzureQueueStorageImpl;
+import com.azure.storage.queue.models.KeyInfo;
import com.azure.storage.queue.models.QueueCorsRule;
import com.azure.storage.queue.models.QueueItem;
import com.azure.storage.queue.models.QueueMessageDecodingError;
@@ -26,9 +29,11 @@
import com.azure.storage.queue.models.QueueServiceStatistics;
import com.azure.storage.queue.models.QueueStorageException;
import com.azure.storage.queue.models.QueuesSegmentOptions;
+import com.azure.storage.queue.models.UserDelegationKey;
import reactor.core.publisher.Mono;
import java.time.Duration;
+import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -709,4 +714,57 @@ public String generateAccountSas(AccountSasSignatureValues accountSasSignatureVa
return new AccountSasImplUtil(accountSasSignatureValues, null)
.generateSas(SasImplUtils.extractSharedKeyCredential(getHttpPipeline()), stringToSignHandler, context);
}
+
+ /**
+ * Gets a user delegation key for use with this account's queue storage. Note: This method call is only valid when
+ * using {@link TokenCredential} in this object's {@link HttpPipeline}.
+ *
+ * @param start Start time for the key's validity. Null indicates immediate start.
+ * @param expiry Expiration of the key's validity.
+ * @return A {@link Mono} containing the user delegation key.
+ * @throws IllegalArgumentException If {@code start} isn't null and is after {@code expiry}.
+ * @throws NullPointerException If {@code expiry} is null.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public Mono getUserDelegationKey(OffsetDateTime start, OffsetDateTime expiry) {
+ return getUserDelegationKeyWithResponse(start, expiry).flatMap(FluxUtil::toMono);
+ }
+
+ /**
+ * Gets a user delegation key for use with this account's queue storage. Note: This method call is only valid when
+ * using {@link TokenCredential} in this object's {@link HttpPipeline}.
+ *
+ * @param start Start time for the key's validity. Null indicates immediate start.
+ * @param expiry Expiration of the key's validity.
+ * @return A {@link Mono} containing a {@link Response} whose {@link Response#getValue() value} containing the user
+ * delegation key.
+ * @throws IllegalArgumentException If {@code start} isn't null and is after {@code expiry}.
+ * @throws NullPointerException If {@code expiry} is null.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public Mono> getUserDelegationKeyWithResponse(OffsetDateTime start,
+ OffsetDateTime expiry) {
+ try {
+ return withContext(context -> getUserDelegationKeyWithResponse(start, expiry, context));
+ } catch (RuntimeException ex) {
+ return monoError(LOGGER, ex);
+ }
+ }
+
+ Mono> getUserDelegationKeyWithResponse(OffsetDateTime start, OffsetDateTime expiry,
+ Context context) {
+ StorageImplUtils.assertNotNull("expiry", expiry);
+ if (start != null && !start.isBefore(expiry)) {
+ throw LOGGER.logExceptionAsError(
+ new IllegalArgumentException("`start` must be null or a datetime before `expiry`."));
+ }
+ context = context == null ? Context.NONE : context;
+
+ return client.getServices()
+ .getUserDelegationKeyWithResponseAsync(
+ new KeyInfo().setStart(start == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(start))
+ .setExpiry(Constants.ISO_8601_UTC_DATE_FORMATTER.format(expiry)),
+ null, null, context)
+ .map(rb -> new SimpleResponse<>(rb, rb.getValue()));
+ }
}
diff --git a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueServiceClient.java b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueServiceClient.java
index c51516b84116..778c0681027d 100644
--- a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueServiceClient.java
+++ b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/QueueServiceClient.java
@@ -5,6 +5,7 @@
import com.azure.core.annotation.ReturnType;
import com.azure.core.annotation.ServiceClient;
import com.azure.core.annotation.ServiceMethod;
+import com.azure.core.credential.TokenCredential;
import com.azure.core.http.HttpPipeline;
import com.azure.core.http.rest.PagedIterable;
import com.azure.core.http.rest.PagedResponse;
@@ -15,10 +16,14 @@
import com.azure.core.util.logging.ClientLogger;
import com.azure.storage.common.StorageSharedKeyCredential;
import com.azure.storage.common.implementation.AccountSasImplUtil;
+import com.azure.storage.common.implementation.Constants;
import com.azure.storage.common.implementation.SasImplUtils;
+import com.azure.storage.common.implementation.StorageImplUtils;
import com.azure.storage.common.sas.AccountSasSignatureValues;
import com.azure.storage.queue.implementation.AzureQueueStorageImpl;
+import com.azure.storage.queue.models.KeyInfo;
import com.azure.storage.queue.implementation.models.ServicesGetStatisticsHeaders;
+import com.azure.storage.queue.implementation.models.ServicesGetUserDelegationKeyHeaders;
import com.azure.storage.queue.models.QueueCorsRule;
import com.azure.storage.queue.models.QueueItem;
import com.azure.storage.queue.models.QueueMessageDecodingError;
@@ -26,18 +31,22 @@
import com.azure.storage.queue.models.QueueServiceStatistics;
import com.azure.storage.queue.models.QueueStorageException;
import com.azure.storage.queue.models.QueuesSegmentOptions;
+import com.azure.storage.queue.models.UserDelegationKey;
import reactor.core.publisher.Mono;
import java.time.Duration;
+import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.concurrent.Callable;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
+import static com.azure.storage.common.implementation.StorageImplUtils.sendRequest;
import static com.azure.storage.common.implementation.StorageImplUtils.submitThreadPool;
/**
@@ -693,4 +702,48 @@ public String generateAccountSas(AccountSasSignatureValues accountSasSignatureVa
return new AccountSasImplUtil(accountSasSignatureValues, null)
.generateSas(SasImplUtils.extractSharedKeyCredential(getHttpPipeline()), stringToSignHandler, context);
}
+
+ /**
+ * Gets a user delegation key for use with this account's queue storage. Note: This method call is only valid when
+ * using {@link TokenCredential} in this object's {@link HttpPipeline}.
+ *
+ * @param start Start time for the key's validity. Null indicates immediate start.
+ * @param expiry Expiration of the key's validity.
+ * @return The user delegation key.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public UserDelegationKey getUserDelegationKey(OffsetDateTime start, OffsetDateTime expiry) {
+ return getUserDelegationKeyWithResponse(start, expiry, null, Context.NONE).getValue();
+ }
+
+ /**
+ * Gets a user delegation key for use with this account's queue storage. Note: This method call is only valid when
+ * using {@link TokenCredential} in this object's {@link HttpPipeline}.
+ *
+ * @param start Start time for the key's validity. Null indicates immediate start.
+ * @param expiry Expiration of the key's validity.
+ * @param timeout An optional timeout value beyond which a {@link RuntimeException} will be raised.
+ * @param context Additional context that is passed through the Http pipeline during the service call.
+ * @return A {@link Response} whose {@link Response#getValue() value} contains the user delegation key.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public Response getUserDelegationKeyWithResponse(OffsetDateTime start, OffsetDateTime expiry,
+ Duration timeout, Context context) {
+ StorageImplUtils.assertNotNull("expiry", expiry);
+ if (start != null && !start.isBefore(expiry)) {
+ throw LOGGER.logExceptionAsError(
+ new IllegalArgumentException("`start` must be null or a datetime before `expiry`."));
+ }
+ Context finalContext = context == null ? Context.NONE : context;
+ Callable> operation
+ = () -> this.azureQueueStorage.getServices()
+ .getUserDelegationKeyWithResponse(
+ new KeyInfo().setStart(start == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(start))
+ .setExpiry(Constants.ISO_8601_UTC_DATE_FORMATTER.format(expiry)),
+ null, null, finalContext);
+ ResponseBase response
+ = sendRequest(operation, timeout, QueueStorageException.class);
+ return new SimpleResponse<>(response, response.getValue());
+ }
+
}
diff --git a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/ServicesImpl.java b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/ServicesImpl.java
index 3e066282d0ef..b5224b043ffd 100644
--- a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/ServicesImpl.java
+++ b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/ServicesImpl.java
@@ -10,6 +10,7 @@
import com.azure.core.annotation.Host;
import com.azure.core.annotation.HostParam;
import com.azure.core.annotation.PathParam;
+import com.azure.core.annotation.Post;
import com.azure.core.annotation.Put;
import com.azure.core.annotation.QueryParam;
import com.azure.core.annotation.ReturnType;
@@ -29,13 +30,16 @@
import com.azure.storage.queue.implementation.models.QueueStorageExceptionInternal;
import com.azure.storage.queue.implementation.models.ServicesGetPropertiesHeaders;
import com.azure.storage.queue.implementation.models.ServicesGetStatisticsHeaders;
+import com.azure.storage.queue.implementation.models.ServicesGetUserDelegationKeyHeaders;
import com.azure.storage.queue.implementation.models.ServicesListQueuesSegmentHeaders;
import com.azure.storage.queue.implementation.models.ServicesListQueuesSegmentNextHeaders;
import com.azure.storage.queue.implementation.models.ServicesSetPropertiesHeaders;
import com.azure.storage.queue.implementation.util.ModelHelper;
+import com.azure.storage.queue.models.KeyInfo;
import com.azure.storage.queue.models.QueueItem;
import com.azure.storage.queue.models.QueueServiceProperties;
import com.azure.storage.queue.models.QueueServiceStatistics;
+import com.azure.storage.queue.models.UserDelegationKey;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -186,6 +190,42 @@ Response getStatisticsNoCustomHeadersSync(@HostParam("ur
@HeaderParam("x-ms-client-request-id") String requestId, @HeaderParam("Accept") String accept,
Context context);
+ @Post("/")
+ @ExpectedResponses({ 200 })
+ @UnexpectedResponseExceptionType(QueueStorageExceptionInternal.class)
+ Mono> getUserDelegationKey(
+ @HostParam("url") String url, @QueryParam("restype") String restype, @QueryParam("comp") String comp,
+ @QueryParam("timeout") Integer timeout, @HeaderParam("x-ms-version") String version,
+ @HeaderParam("x-ms-client-request-id") String requestId, @BodyParam("application/xml") KeyInfo keyInfo,
+ @HeaderParam("Accept") String accept, Context context);
+
+ @Post("/")
+ @ExpectedResponses({ 200 })
+ @UnexpectedResponseExceptionType(QueueStorageExceptionInternal.class)
+ Mono> getUserDelegationKeyNoCustomHeaders(@HostParam("url") String url,
+ @QueryParam("restype") String restype, @QueryParam("comp") String comp,
+ @QueryParam("timeout") Integer timeout, @HeaderParam("x-ms-version") String version,
+ @HeaderParam("x-ms-client-request-id") String requestId, @BodyParam("application/xml") KeyInfo keyInfo,
+ @HeaderParam("Accept") String accept, Context context);
+
+ @Post("/")
+ @ExpectedResponses({ 200 })
+ @UnexpectedResponseExceptionType(QueueStorageExceptionInternal.class)
+ ResponseBase getUserDelegationKeySync(
+ @HostParam("url") String url, @QueryParam("restype") String restype, @QueryParam("comp") String comp,
+ @QueryParam("timeout") Integer timeout, @HeaderParam("x-ms-version") String version,
+ @HeaderParam("x-ms-client-request-id") String requestId, @BodyParam("application/xml") KeyInfo keyInfo,
+ @HeaderParam("Accept") String accept, Context context);
+
+ @Post("/")
+ @ExpectedResponses({ 200 })
+ @UnexpectedResponseExceptionType(QueueStorageExceptionInternal.class)
+ Response getUserDelegationKeyNoCustomHeadersSync(@HostParam("url") String url,
+ @QueryParam("restype") String restype, @QueryParam("comp") String comp,
+ @QueryParam("timeout") Integer timeout, @HeaderParam("x-ms-version") String version,
+ @HeaderParam("x-ms-client-request-id") String requestId, @BodyParam("application/xml") KeyInfo keyInfo,
+ @HeaderParam("Accept") String accept, Context context);
+
@Get("/")
@ExpectedResponses({ 200 })
@UnexpectedResponseExceptionType(QueueStorageExceptionInternal.class)
@@ -939,6 +979,239 @@ public Response getStatisticsNoCustomHeadersWithResponse
}
}
+ /**
+ * Retrieves a user delegation key for the Queue service. This is only a valid operation when using bearer token
+ * authentication.
+ *
+ * @param keyInfo Key information.
+ * @param timeout The The timeout parameter is expressed in seconds. For more information, see <a
+ * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-queue-service-operations>Setting
+ * Timeouts for Queue Service Operations.</a>.
+ * @param requestId Provides a client-generated, opaque value with a 1 KB character limit that is recorded in the
+ * analytics logs when storage analytics logging is enabled.
+ * @throws IllegalArgumentException thrown if parameters fail the validation.
+ * @throws QueueStorageExceptionInternal thrown if the request is rejected by server.
+ * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
+ * @return a user delegation key along with {@link ResponseBase} on successful completion of {@link Mono}.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public Mono>
+ getUserDelegationKeyWithResponseAsync(KeyInfo keyInfo, Integer timeout, String requestId) {
+ return FluxUtil
+ .withContext(context -> getUserDelegationKeyWithResponseAsync(keyInfo, timeout, requestId, context))
+ .onErrorMap(QueueStorageExceptionInternal.class, ModelHelper::mapToQueueStorageException);
+ }
+
+ /**
+ * Retrieves a user delegation key for the Queue service. This is only a valid operation when using bearer token
+ * authentication.
+ *
+ * @param keyInfo Key information.
+ * @param timeout The The timeout parameter is expressed in seconds. For more information, see <a
+ * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-queue-service-operations>Setting
+ * Timeouts for Queue Service Operations.</a>.
+ * @param requestId Provides a client-generated, opaque value with a 1 KB character limit that is recorded in the
+ * analytics logs when storage analytics logging is enabled.
+ * @param context The context to associate with this operation.
+ * @throws IllegalArgumentException thrown if parameters fail the validation.
+ * @throws QueueStorageExceptionInternal thrown if the request is rejected by server.
+ * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
+ * @return a user delegation key along with {@link ResponseBase} on successful completion of {@link Mono}.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public Mono>
+ getUserDelegationKeyWithResponseAsync(KeyInfo keyInfo, Integer timeout, String requestId, Context context) {
+ final String restype = "service";
+ final String comp = "userdelegationkey";
+ final String accept = "application/xml";
+ return service
+ .getUserDelegationKey(this.client.getUrl(), restype, comp, timeout, this.client.getVersion(), requestId,
+ keyInfo, accept, context)
+ .onErrorMap(QueueStorageExceptionInternal.class, ModelHelper::mapToQueueStorageException);
+ }
+
+ /**
+ * Retrieves a user delegation key for the Queue service. This is only a valid operation when using bearer token
+ * authentication.
+ *
+ * @param keyInfo Key information.
+ * @param timeout The The timeout parameter is expressed in seconds. For more information, see <a
+ * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-queue-service-operations>Setting
+ * Timeouts for Queue Service Operations.</a>.
+ * @param requestId Provides a client-generated, opaque value with a 1 KB character limit that is recorded in the
+ * analytics logs when storage analytics logging is enabled.
+ * @throws IllegalArgumentException thrown if parameters fail the validation.
+ * @throws QueueStorageExceptionInternal thrown if the request is rejected by server.
+ * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
+ * @return a user delegation key on successful completion of {@link Mono}.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public Mono getUserDelegationKeyAsync(KeyInfo keyInfo, Integer timeout, String requestId) {
+ return getUserDelegationKeyWithResponseAsync(keyInfo, timeout, requestId)
+ .onErrorMap(QueueStorageExceptionInternal.class, ModelHelper::mapToQueueStorageException)
+ .flatMap(res -> Mono.justOrEmpty(res.getValue()));
+ }
+
+ /**
+ * Retrieves a user delegation key for the Queue service. This is only a valid operation when using bearer token
+ * authentication.
+ *
+ * @param keyInfo Key information.
+ * @param timeout The The timeout parameter is expressed in seconds. For more information, see <a
+ * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-queue-service-operations>Setting
+ * Timeouts for Queue Service Operations.</a>.
+ * @param requestId Provides a client-generated, opaque value with a 1 KB character limit that is recorded in the
+ * analytics logs when storage analytics logging is enabled.
+ * @param context The context to associate with this operation.
+ * @throws IllegalArgumentException thrown if parameters fail the validation.
+ * @throws QueueStorageExceptionInternal thrown if the request is rejected by server.
+ * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
+ * @return a user delegation key on successful completion of {@link Mono}.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public Mono getUserDelegationKeyAsync(KeyInfo keyInfo, Integer timeout, String requestId,
+ Context context) {
+ return getUserDelegationKeyWithResponseAsync(keyInfo, timeout, requestId, context)
+ .onErrorMap(QueueStorageExceptionInternal.class, ModelHelper::mapToQueueStorageException)
+ .flatMap(res -> Mono.justOrEmpty(res.getValue()));
+ }
+
+ /**
+ * Retrieves a user delegation key for the Queue service. This is only a valid operation when using bearer token
+ * authentication.
+ *
+ * @param keyInfo Key information.
+ * @param timeout The The timeout parameter is expressed in seconds. For more information, see <a
+ * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-queue-service-operations>Setting
+ * Timeouts for Queue Service Operations.</a>.
+ * @param requestId Provides a client-generated, opaque value with a 1 KB character limit that is recorded in the
+ * analytics logs when storage analytics logging is enabled.
+ * @throws IllegalArgumentException thrown if parameters fail the validation.
+ * @throws QueueStorageExceptionInternal thrown if the request is rejected by server.
+ * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
+ * @return a user delegation key along with {@link Response} on successful completion of {@link Mono}.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public Mono> getUserDelegationKeyNoCustomHeadersWithResponseAsync(KeyInfo keyInfo,
+ Integer timeout, String requestId) {
+ return FluxUtil
+ .withContext(
+ context -> getUserDelegationKeyNoCustomHeadersWithResponseAsync(keyInfo, timeout, requestId, context))
+ .onErrorMap(QueueStorageExceptionInternal.class, ModelHelper::mapToQueueStorageException);
+ }
+
+ /**
+ * Retrieves a user delegation key for the Queue service. This is only a valid operation when using bearer token
+ * authentication.
+ *
+ * @param keyInfo Key information.
+ * @param timeout The The timeout parameter is expressed in seconds. For more information, see <a
+ * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-queue-service-operations>Setting
+ * Timeouts for Queue Service Operations.</a>.
+ * @param requestId Provides a client-generated, opaque value with a 1 KB character limit that is recorded in the
+ * analytics logs when storage analytics logging is enabled.
+ * @param context The context to associate with this operation.
+ * @throws IllegalArgumentException thrown if parameters fail the validation.
+ * @throws QueueStorageExceptionInternal thrown if the request is rejected by server.
+ * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
+ * @return a user delegation key along with {@link Response} on successful completion of {@link Mono}.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public Mono> getUserDelegationKeyNoCustomHeadersWithResponseAsync(KeyInfo keyInfo,
+ Integer timeout, String requestId, Context context) {
+ final String restype = "service";
+ final String comp = "userdelegationkey";
+ final String accept = "application/xml";
+ return service
+ .getUserDelegationKeyNoCustomHeaders(this.client.getUrl(), restype, comp, timeout, this.client.getVersion(),
+ requestId, keyInfo, accept, context)
+ .onErrorMap(QueueStorageExceptionInternal.class, ModelHelper::mapToQueueStorageException);
+ }
+
+ /**
+ * Retrieves a user delegation key for the Queue service. This is only a valid operation when using bearer token
+ * authentication.
+ *
+ * @param keyInfo Key information.
+ * @param timeout The The timeout parameter is expressed in seconds. For more information, see <a
+ * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-queue-service-operations>Setting
+ * Timeouts for Queue Service Operations.</a>.
+ * @param requestId Provides a client-generated, opaque value with a 1 KB character limit that is recorded in the
+ * analytics logs when storage analytics logging is enabled.
+ * @param context The context to associate with this operation.
+ * @throws IllegalArgumentException thrown if parameters fail the validation.
+ * @throws QueueStorageExceptionInternal thrown if the request is rejected by server.
+ * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
+ * @return a user delegation key along with {@link ResponseBase}.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public ResponseBase
+ getUserDelegationKeyWithResponse(KeyInfo keyInfo, Integer timeout, String requestId, Context context) {
+ try {
+ final String restype = "service";
+ final String comp = "userdelegationkey";
+ final String accept = "application/xml";
+ return service.getUserDelegationKeySync(this.client.getUrl(), restype, comp, timeout,
+ this.client.getVersion(), requestId, keyInfo, accept, context);
+ } catch (QueueStorageExceptionInternal internalException) {
+ throw ModelHelper.mapToQueueStorageException(internalException);
+ }
+ }
+
+ /**
+ * Retrieves a user delegation key for the Queue service. This is only a valid operation when using bearer token
+ * authentication.
+ *
+ * @param keyInfo Key information.
+ * @param timeout The The timeout parameter is expressed in seconds. For more information, see <a
+ * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-queue-service-operations>Setting
+ * Timeouts for Queue Service Operations.</a>.
+ * @param requestId Provides a client-generated, opaque value with a 1 KB character limit that is recorded in the
+ * analytics logs when storage analytics logging is enabled.
+ * @throws IllegalArgumentException thrown if parameters fail the validation.
+ * @throws QueueStorageExceptionInternal thrown if the request is rejected by server.
+ * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
+ * @return a user delegation key.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public UserDelegationKey getUserDelegationKey(KeyInfo keyInfo, Integer timeout, String requestId) {
+ try {
+ return getUserDelegationKeyWithResponse(keyInfo, timeout, requestId, Context.NONE).getValue();
+ } catch (QueueStorageExceptionInternal internalException) {
+ throw ModelHelper.mapToQueueStorageException(internalException);
+ }
+ }
+
+ /**
+ * Retrieves a user delegation key for the Queue service. This is only a valid operation when using bearer token
+ * authentication.
+ *
+ * @param keyInfo Key information.
+ * @param timeout The The timeout parameter is expressed in seconds. For more information, see <a
+ * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-queue-service-operations>Setting
+ * Timeouts for Queue Service Operations.</a>.
+ * @param requestId Provides a client-generated, opaque value with a 1 KB character limit that is recorded in the
+ * analytics logs when storage analytics logging is enabled.
+ * @param context The context to associate with this operation.
+ * @throws IllegalArgumentException thrown if parameters fail the validation.
+ * @throws QueueStorageExceptionInternal thrown if the request is rejected by server.
+ * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent.
+ * @return a user delegation key along with {@link Response}.
+ */
+ @ServiceMethod(returns = ReturnType.SINGLE)
+ public Response getUserDelegationKeyNoCustomHeadersWithResponse(KeyInfo keyInfo, Integer timeout,
+ String requestId, Context context) {
+ try {
+ final String restype = "service";
+ final String comp = "userdelegationkey";
+ final String accept = "application/xml";
+ return service.getUserDelegationKeyNoCustomHeadersSync(this.client.getUrl(), restype, comp, timeout,
+ this.client.getVersion(), requestId, keyInfo, accept, context);
+ } catch (QueueStorageExceptionInternal internalException) {
+ throw ModelHelper.mapToQueueStorageException(internalException);
+ }
+ }
+
/**
* The List Queues Segment operation returns a list of the queues under the specified account.
*
diff --git a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/models/ServicesGetUserDelegationKeyHeaders.java b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/models/ServicesGetUserDelegationKeyHeaders.java
new file mode 100644
index 000000000000..83c3124e9c4f
--- /dev/null
+++ b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/models/ServicesGetUserDelegationKeyHeaders.java
@@ -0,0 +1,157 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+// Code generated by Microsoft (R) AutoRest Code Generator.
+
+package com.azure.storage.queue.implementation.models;
+
+import com.azure.core.annotation.Fluent;
+import com.azure.core.annotation.Generated;
+import com.azure.core.http.HttpHeaderName;
+import com.azure.core.http.HttpHeaders;
+import com.azure.core.util.DateTimeRfc1123;
+import java.time.OffsetDateTime;
+
+/**
+ * The ServicesGetUserDelegationKeyHeaders model.
+ */
+@Fluent
+public final class ServicesGetUserDelegationKeyHeaders {
+ /*
+ * The x-ms-version property.
+ */
+ @Generated
+ private String xMsVersion;
+
+ /*
+ * The x-ms-request-id property.
+ */
+ @Generated
+ private String xMsRequestId;
+
+ /*
+ * The x-ms-client-request-id property.
+ */
+ @Generated
+ private String xMsClientRequestId;
+
+ /*
+ * The Date property.
+ */
+ @Generated
+ private DateTimeRfc1123 date;
+
+ private static final HttpHeaderName X_MS_VERSION = HttpHeaderName.fromString("x-ms-version");
+
+ // HttpHeaders containing the raw property values.
+ /**
+ * Creates an instance of ServicesGetUserDelegationKeyHeaders class.
+ *
+ * @param rawHeaders The raw HttpHeaders that will be used to create the property values.
+ */
+ public ServicesGetUserDelegationKeyHeaders(HttpHeaders rawHeaders) {
+ this.xMsVersion = rawHeaders.getValue(X_MS_VERSION);
+ this.xMsRequestId = rawHeaders.getValue(HttpHeaderName.X_MS_REQUEST_ID);
+ this.xMsClientRequestId = rawHeaders.getValue(HttpHeaderName.X_MS_CLIENT_REQUEST_ID);
+ String date = rawHeaders.getValue(HttpHeaderName.DATE);
+ if (date != null) {
+ this.date = new DateTimeRfc1123(date);
+ } else {
+ this.date = null;
+ }
+ }
+
+ /**
+ * Get the xMsVersion property: The x-ms-version property.
+ *
+ * @return the xMsVersion value.
+ */
+ @Generated
+ public String getXMsVersion() {
+ return this.xMsVersion;
+ }
+
+ /**
+ * Set the xMsVersion property: The x-ms-version property.
+ *
+ * @param xMsVersion the xMsVersion value to set.
+ * @return the ServicesGetUserDelegationKeyHeaders object itself.
+ */
+ @Generated
+ public ServicesGetUserDelegationKeyHeaders setXMsVersion(String xMsVersion) {
+ this.xMsVersion = xMsVersion;
+ return this;
+ }
+
+ /**
+ * Get the xMsRequestId property: The x-ms-request-id property.
+ *
+ * @return the xMsRequestId value.
+ */
+ @Generated
+ public String getXMsRequestId() {
+ return this.xMsRequestId;
+ }
+
+ /**
+ * Set the xMsRequestId property: The x-ms-request-id property.
+ *
+ * @param xMsRequestId the xMsRequestId value to set.
+ * @return the ServicesGetUserDelegationKeyHeaders object itself.
+ */
+ @Generated
+ public ServicesGetUserDelegationKeyHeaders setXMsRequestId(String xMsRequestId) {
+ this.xMsRequestId = xMsRequestId;
+ return this;
+ }
+
+ /**
+ * Get the xMsClientRequestId property: The x-ms-client-request-id property.
+ *
+ * @return the xMsClientRequestId value.
+ */
+ @Generated
+ public String getXMsClientRequestId() {
+ return this.xMsClientRequestId;
+ }
+
+ /**
+ * Set the xMsClientRequestId property: The x-ms-client-request-id property.
+ *
+ * @param xMsClientRequestId the xMsClientRequestId value to set.
+ * @return the ServicesGetUserDelegationKeyHeaders object itself.
+ */
+ @Generated
+ public ServicesGetUserDelegationKeyHeaders setXMsClientRequestId(String xMsClientRequestId) {
+ this.xMsClientRequestId = xMsClientRequestId;
+ return this;
+ }
+
+ /**
+ * Get the date property: The Date property.
+ *
+ * @return the date value.
+ */
+ @Generated
+ public OffsetDateTime getDate() {
+ if (this.date == null) {
+ return null;
+ }
+ return this.date.getDateTime();
+ }
+
+ /**
+ * Set the date property: The Date property.
+ *
+ * @param date the date value to set.
+ * @return the ServicesGetUserDelegationKeyHeaders object itself.
+ */
+ @Generated
+ public ServicesGetUserDelegationKeyHeaders setDate(OffsetDateTime date) {
+ if (date == null) {
+ this.date = null;
+ } else {
+ this.date = new DateTimeRfc1123(date);
+ }
+ return this;
+ }
+}
diff --git a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/util/QueueSasImplUtil.java b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/util/QueueSasImplUtil.java
index d79c87724d4e..c353b6690fb8 100644
--- a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/util/QueueSasImplUtil.java
+++ b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/implementation/util/QueueSasImplUtil.java
@@ -13,6 +13,7 @@
import com.azure.storage.common.sas.SasIpRange;
import com.azure.storage.common.sas.SasProtocol;
import com.azure.storage.queue.QueueServiceVersion;
+import com.azure.storage.queue.models.UserDelegationKey;
import com.azure.storage.queue.sas.QueueSasPermission;
import com.azure.storage.queue.sas.QueueServiceSasSignatureValues;
@@ -36,18 +37,13 @@ public class QueueSasImplUtil {
.get(Constants.PROPERTY_AZURE_STORAGE_SAS_SERVICE_VERSION, QueueServiceVersion.getLatest().getVersion());
private SasProtocol protocol;
-
private OffsetDateTime startTime;
-
private OffsetDateTime expiryTime;
-
private String permissions;
-
private SasIpRange sasIpRange;
-
private String queueName;
-
private String identifier;
+ private String delegatedUserObjectId;
/**
* Creates a new {@link QueueSasImplUtil} with the specified parameters
@@ -64,6 +60,7 @@ public QueueSasImplUtil(QueueServiceSasSignatureValues sasValues, String queueNa
this.sasIpRange = sasValues.getSasIpRange();
this.queueName = queueName;
this.identifier = sasValues.getIdentifier();
+ this.delegatedUserObjectId = sasValues.getDelegatedUserObjectId();
}
/**
@@ -102,10 +99,40 @@ public String generateSas(StorageSharedKeyCredential storageSharedKeyCredentials
stringToSignHandler.accept(stringToSign);
}
- return encode(signature);
+ return encode(null /* userDelegationKey */, signature);
+ }
+
+ /**
+ * Generates a Sas signed with a {@link UserDelegationKey}
+ *
+ * @param delegationKey {@link UserDelegationKey}
+ * @param accountName The account name
+ * @param stringToSignHandler For debugging purposes only. Returns the string to sign that was used to generate the
+ * signature.
+ * @param context Additional context that is passed through the code when generating a SAS.
+ * @return A String representing the Sas
+ */
+ public String generateUserDelegationSas(UserDelegationKey delegationKey, String accountName,
+ Consumer stringToSignHandler, Context context) {
+ StorageImplUtils.assertNotNull("delegationKey", delegationKey);
+ StorageImplUtils.assertNotNull("accountName", accountName);
+
+ ensureState();
+
+ // Signature is generated on the un-url-encoded values.
+ final String canonicalName = getCanonicalName(accountName);
+ final String stringToSign = stringToSign(delegationKey, canonicalName);
+ StorageImplUtils.logStringToSign(LOGGER, stringToSign, context);
+ String signature = StorageImplUtils.computeHMac256(delegationKey.getValue(), stringToSign);
+
+ if (stringToSignHandler != null) {
+ stringToSignHandler.accept(stringToSign);
+ }
+
+ return encode(delegationKey, signature);
}
- private String encode(String signature) {
+ private String encode(UserDelegationKey userDelegationKey, String signature) {
/*
We should be url-encoding each key and each value, but because we know all the keys and values will encode to
themselves, we cheat except for the signature value.
@@ -120,6 +147,22 @@ private String encode(String signature) {
formatQueryParameterDate(new TimeAndFormat(this.expiryTime, null)));
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_IP_RANGE, this.sasIpRange);
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_IDENTIFIER, this.identifier);
+ if (userDelegationKey != null) {
+ tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_OBJECT_ID,
+ userDelegationKey.getSignedObjectId());
+ tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_TENANT_ID,
+ userDelegationKey.getSignedTenantId());
+ tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_KEY_START,
+ formatQueryParameterDate(new TimeAndFormat(userDelegationKey.getSignedStart(), null)));
+ tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_KEY_EXPIRY,
+ formatQueryParameterDate(new TimeAndFormat(userDelegationKey.getSignedExpiry(), null)));
+ tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_KEY_SERVICE,
+ userDelegationKey.getSignedService());
+ tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_KEY_VERSION,
+ userDelegationKey.getSignedVersion());
+ tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_DELEGATED_USER_OBJECT_ID,
+ this.delegatedUserObjectId);
+ }
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNED_PERMISSIONS, this.permissions);
tryAppendQueryParameter(sb, Constants.UrlConstants.SAS_SIGNATURE, signature);
@@ -169,4 +212,19 @@ private String stringToSign(String canonicalName) {
this.protocol == null ? "" : protocol.toString(), VERSION == null ? "" : VERSION);
}
+ private String stringToSign(final UserDelegationKey key, String canonicalName) {
+ return String.join("\n", this.permissions == null ? "" : this.permissions,
+ this.startTime == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(this.startTime),
+ this.expiryTime == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(this.expiryTime), canonicalName,
+ key.getSignedObjectId() == null ? "" : key.getSignedObjectId(),
+ key.getSignedTenantId() == null ? "" : key.getSignedTenantId(),
+ key.getSignedStart() == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(key.getSignedStart()),
+ key.getSignedExpiry() == null ? "" : Constants.ISO_8601_UTC_DATE_FORMATTER.format(key.getSignedExpiry()),
+ key.getSignedService() == null ? "" : key.getSignedService(),
+ key.getSignedVersion() == null ? "" : key.getSignedVersion(), "", // SignedKeyDelegatedUserTenantId, will be added in a future release.
+ this.delegatedUserObjectId == null ? "" : this.delegatedUserObjectId,
+ this.sasIpRange == null ? "" : this.sasIpRange.toString(),
+ this.protocol == null ? "" : this.protocol.toString(), VERSION);
+ }
+
}
diff --git a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/models/KeyInfo.java b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/models/KeyInfo.java
new file mode 100644
index 000000000000..330812896c7a
--- /dev/null
+++ b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/models/KeyInfo.java
@@ -0,0 +1,144 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+// Code generated by Microsoft (R) AutoRest Code Generator.
+
+package com.azure.storage.queue.models;
+
+import com.azure.core.annotation.Fluent;
+import com.azure.core.annotation.Generated;
+import com.azure.xml.XmlReader;
+import com.azure.xml.XmlSerializable;
+import com.azure.xml.XmlToken;
+import com.azure.xml.XmlWriter;
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLStreamException;
+
+/**
+ * Key information.
+ */
+@Fluent
+public final class KeyInfo implements XmlSerializable {
+ /*
+ * The date-time the key is active in ISO 8601 UTC time
+ */
+ @Generated
+ private String start;
+
+ /*
+ * The date-time the key expires in ISO 8601 UTC time
+ */
+ @Generated
+ private String expiry;
+
+ /**
+ * Creates an instance of KeyInfo class.
+ */
+ @Generated
+ public KeyInfo() {
+ }
+
+ /**
+ * Get the start property: The date-time the key is active in ISO 8601 UTC time.
+ *
+ * @return the start value.
+ */
+ @Generated
+ public String getStart() {
+ return this.start;
+ }
+
+ /**
+ * Set the start property: The date-time the key is active in ISO 8601 UTC time.
+ *
+ * @param start the start value to set.
+ * @return the KeyInfo object itself.
+ */
+ @Generated
+ public KeyInfo setStart(String start) {
+ this.start = start;
+ return this;
+ }
+
+ /**
+ * Get the expiry property: The date-time the key expires in ISO 8601 UTC time.
+ *
+ * @return the expiry value.
+ */
+ @Generated
+ public String getExpiry() {
+ return this.expiry;
+ }
+
+ /**
+ * Set the expiry property: The date-time the key expires in ISO 8601 UTC time.
+ *
+ * @param expiry the expiry value to set.
+ * @return the KeyInfo object itself.
+ */
+ @Generated
+ public KeyInfo setExpiry(String expiry) {
+ this.expiry = expiry;
+ return this;
+ }
+
+ @Generated
+ @Override
+ public XmlWriter toXml(XmlWriter xmlWriter) throws XMLStreamException {
+ return toXml(xmlWriter, null);
+ }
+
+ @Generated
+ @Override
+ public XmlWriter toXml(XmlWriter xmlWriter, String rootElementName) throws XMLStreamException {
+ rootElementName = rootElementName == null || rootElementName.isEmpty() ? "KeyInfo" : rootElementName;
+ xmlWriter.writeStartElement(rootElementName);
+ xmlWriter.writeStringElement("Start", this.start);
+ xmlWriter.writeStringElement("Expiry", this.expiry);
+ return xmlWriter.writeEndElement();
+ }
+
+ /**
+ * Reads an instance of KeyInfo from the XmlReader.
+ *
+ * @param xmlReader The XmlReader being read.
+ * @return An instance of KeyInfo if the XmlReader was pointing to an instance of it, or null if it was pointing to
+ * XML null.
+ * @throws XMLStreamException If an error occurs while reading the KeyInfo.
+ */
+ @Generated
+ public static KeyInfo fromXml(XmlReader xmlReader) throws XMLStreamException {
+ return fromXml(xmlReader, null);
+ }
+
+ /**
+ * Reads an instance of KeyInfo from the XmlReader.
+ *
+ * @param xmlReader The XmlReader being read.
+ * @param rootElementName Optional root element name to override the default defined by the model. Used to support
+ * cases where the model can deserialize from different root element names.
+ * @return An instance of KeyInfo if the XmlReader was pointing to an instance of it, or null if it was pointing to
+ * XML null.
+ * @throws XMLStreamException If an error occurs while reading the KeyInfo.
+ */
+ @Generated
+ public static KeyInfo fromXml(XmlReader xmlReader, String rootElementName) throws XMLStreamException {
+ String finalRootElementName
+ = rootElementName == null || rootElementName.isEmpty() ? "KeyInfo" : rootElementName;
+ return xmlReader.readObject(finalRootElementName, reader -> {
+ KeyInfo deserializedKeyInfo = new KeyInfo();
+ while (reader.nextElement() != XmlToken.END_ELEMENT) {
+ QName elementName = reader.getElementName();
+
+ if ("Start".equals(elementName.getLocalPart())) {
+ deserializedKeyInfo.start = reader.getStringElement();
+ } else if ("Expiry".equals(elementName.getLocalPart())) {
+ deserializedKeyInfo.expiry = reader.getStringElement();
+ } else {
+ reader.skipElement();
+ }
+ }
+
+ return deserializedKeyInfo;
+ });
+ }
+}
diff --git a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/models/UserDelegationKey.java b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/models/UserDelegationKey.java
new file mode 100644
index 000000000000..1fb20470a5ac
--- /dev/null
+++ b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/models/UserDelegationKey.java
@@ -0,0 +1,306 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+// Code generated by Microsoft (R) AutoRest Code Generator.
+
+package com.azure.storage.queue.models;
+
+import com.azure.core.annotation.Fluent;
+import com.azure.core.annotation.Generated;
+import com.azure.core.util.CoreUtils;
+import com.azure.xml.XmlReader;
+import com.azure.xml.XmlSerializable;
+import com.azure.xml.XmlToken;
+import com.azure.xml.XmlWriter;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLStreamException;
+
+/**
+ * A user delegation key.
+ */
+@Fluent
+public final class UserDelegationKey implements XmlSerializable {
+ /*
+ * The Azure Active Directory object ID in GUID format.
+ */
+ @Generated
+ private String signedObjectId;
+
+ /*
+ * The Azure Active Directory tenant ID in GUID format
+ */
+ @Generated
+ private String signedTenantId;
+
+ /*
+ * The date-time the key is active
+ */
+ @Generated
+ private OffsetDateTime signedStart;
+
+ /*
+ * The date-time the key expires
+ */
+ @Generated
+ private OffsetDateTime signedExpiry;
+
+ /*
+ * Abbreviation of the Azure Storage service that accepts the key
+ */
+ @Generated
+ private String signedService;
+
+ /*
+ * The service version that created the key
+ */
+ @Generated
+ private String signedVersion;
+
+ /*
+ * The key as a base64 string
+ */
+ @Generated
+ private String value;
+
+ /**
+ * Creates an instance of UserDelegationKey class.
+ */
+ @Generated
+ public UserDelegationKey() {
+ }
+
+ /**
+ * Get the signedObjectId property: The Azure Active Directory object ID in GUID format.
+ *
+ * @return the signedObjectId value.
+ */
+ @Generated
+ public String getSignedObjectId() {
+ return this.signedObjectId;
+ }
+
+ /**
+ * Set the signedObjectId property: The Azure Active Directory object ID in GUID format.
+ *
+ * @param signedObjectId the signedObjectId value to set.
+ * @return the UserDelegationKey object itself.
+ */
+ @Generated
+ public UserDelegationKey setSignedObjectId(String signedObjectId) {
+ this.signedObjectId = signedObjectId;
+ return this;
+ }
+
+ /**
+ * Get the signedTenantId property: The Azure Active Directory tenant ID in GUID format.
+ *
+ * @return the signedTenantId value.
+ */
+ @Generated
+ public String getSignedTenantId() {
+ return this.signedTenantId;
+ }
+
+ /**
+ * Set the signedTenantId property: The Azure Active Directory tenant ID in GUID format.
+ *
+ * @param signedTenantId the signedTenantId value to set.
+ * @return the UserDelegationKey object itself.
+ */
+ @Generated
+ public UserDelegationKey setSignedTenantId(String signedTenantId) {
+ this.signedTenantId = signedTenantId;
+ return this;
+ }
+
+ /**
+ * Get the signedStart property: The date-time the key is active.
+ *
+ * @return the signedStart value.
+ */
+ @Generated
+ public OffsetDateTime getSignedStart() {
+ return this.signedStart;
+ }
+
+ /**
+ * Set the signedStart property: The date-time the key is active.
+ *
+ * @param signedStart the signedStart value to set.
+ * @return the UserDelegationKey object itself.
+ */
+ @Generated
+ public UserDelegationKey setSignedStart(OffsetDateTime signedStart) {
+ this.signedStart = signedStart;
+ return this;
+ }
+
+ /**
+ * Get the signedExpiry property: The date-time the key expires.
+ *
+ * @return the signedExpiry value.
+ */
+ @Generated
+ public OffsetDateTime getSignedExpiry() {
+ return this.signedExpiry;
+ }
+
+ /**
+ * Set the signedExpiry property: The date-time the key expires.
+ *
+ * @param signedExpiry the signedExpiry value to set.
+ * @return the UserDelegationKey object itself.
+ */
+ @Generated
+ public UserDelegationKey setSignedExpiry(OffsetDateTime signedExpiry) {
+ this.signedExpiry = signedExpiry;
+ return this;
+ }
+
+ /**
+ * Get the signedService property: Abbreviation of the Azure Storage service that accepts the key.
+ *
+ * @return the signedService value.
+ */
+ @Generated
+ public String getSignedService() {
+ return this.signedService;
+ }
+
+ /**
+ * Set the signedService property: Abbreviation of the Azure Storage service that accepts the key.
+ *
+ * @param signedService the signedService value to set.
+ * @return the UserDelegationKey object itself.
+ */
+ @Generated
+ public UserDelegationKey setSignedService(String signedService) {
+ this.signedService = signedService;
+ return this;
+ }
+
+ /**
+ * Get the signedVersion property: The service version that created the key.
+ *
+ * @return the signedVersion value.
+ */
+ @Generated
+ public String getSignedVersion() {
+ return this.signedVersion;
+ }
+
+ /**
+ * Set the signedVersion property: The service version that created the key.
+ *
+ * @param signedVersion the signedVersion value to set.
+ * @return the UserDelegationKey object itself.
+ */
+ @Generated
+ public UserDelegationKey setSignedVersion(String signedVersion) {
+ this.signedVersion = signedVersion;
+ return this;
+ }
+
+ /**
+ * Get the value property: The key as a base64 string.
+ *
+ * @return the value value.
+ */
+ @Generated
+ public String getValue() {
+ return this.value;
+ }
+
+ /**
+ * Set the value property: The key as a base64 string.
+ *
+ * @param value the value value to set.
+ * @return the UserDelegationKey object itself.
+ */
+ @Generated
+ public UserDelegationKey setValue(String value) {
+ this.value = value;
+ return this;
+ }
+
+ @Generated
+ @Override
+ public XmlWriter toXml(XmlWriter xmlWriter) throws XMLStreamException {
+ return toXml(xmlWriter, null);
+ }
+
+ @Generated
+ @Override
+ public XmlWriter toXml(XmlWriter xmlWriter, String rootElementName) throws XMLStreamException {
+ rootElementName = rootElementName == null || rootElementName.isEmpty() ? "UserDelegationKey" : rootElementName;
+ xmlWriter.writeStartElement(rootElementName);
+ xmlWriter.writeStringElement("SignedOid", this.signedObjectId);
+ xmlWriter.writeStringElement("SignedTid", this.signedTenantId);
+ xmlWriter.writeStringElement("SignedStart",
+ this.signedStart == null ? null : DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(this.signedStart));
+ xmlWriter.writeStringElement("SignedExpiry",
+ this.signedExpiry == null ? null : DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(this.signedExpiry));
+ xmlWriter.writeStringElement("SignedService", this.signedService);
+ xmlWriter.writeStringElement("SignedVersion", this.signedVersion);
+ xmlWriter.writeStringElement("Value", this.value);
+ return xmlWriter.writeEndElement();
+ }
+
+ /**
+ * Reads an instance of UserDelegationKey from the XmlReader.
+ *
+ * @param xmlReader The XmlReader being read.
+ * @return An instance of UserDelegationKey if the XmlReader was pointing to an instance of it, or null if it was
+ * pointing to XML null.
+ * @throws XMLStreamException If an error occurs while reading the UserDelegationKey.
+ */
+ @Generated
+ public static UserDelegationKey fromXml(XmlReader xmlReader) throws XMLStreamException {
+ return fromXml(xmlReader, null);
+ }
+
+ /**
+ * Reads an instance of UserDelegationKey from the XmlReader.
+ *
+ * @param xmlReader The XmlReader being read.
+ * @param rootElementName Optional root element name to override the default defined by the model. Used to support
+ * cases where the model can deserialize from different root element names.
+ * @return An instance of UserDelegationKey if the XmlReader was pointing to an instance of it, or null if it was
+ * pointing to XML null.
+ * @throws XMLStreamException If an error occurs while reading the UserDelegationKey.
+ */
+ @Generated
+ public static UserDelegationKey fromXml(XmlReader xmlReader, String rootElementName) throws XMLStreamException {
+ String finalRootElementName
+ = rootElementName == null || rootElementName.isEmpty() ? "UserDelegationKey" : rootElementName;
+ return xmlReader.readObject(finalRootElementName, reader -> {
+ UserDelegationKey deserializedUserDelegationKey = new UserDelegationKey();
+ while (reader.nextElement() != XmlToken.END_ELEMENT) {
+ QName elementName = reader.getElementName();
+
+ if ("SignedOid".equals(elementName.getLocalPart())) {
+ deserializedUserDelegationKey.signedObjectId = reader.getStringElement();
+ } else if ("SignedTid".equals(elementName.getLocalPart())) {
+ deserializedUserDelegationKey.signedTenantId = reader.getStringElement();
+ } else if ("SignedStart".equals(elementName.getLocalPart())) {
+ deserializedUserDelegationKey.signedStart
+ = reader.getNullableElement(dateString -> CoreUtils.parseBestOffsetDateTime(dateString));
+ } else if ("SignedExpiry".equals(elementName.getLocalPart())) {
+ deserializedUserDelegationKey.signedExpiry
+ = reader.getNullableElement(dateString -> CoreUtils.parseBestOffsetDateTime(dateString));
+ } else if ("SignedService".equals(elementName.getLocalPart())) {
+ deserializedUserDelegationKey.signedService = reader.getStringElement();
+ } else if ("SignedVersion".equals(elementName.getLocalPart())) {
+ deserializedUserDelegationKey.signedVersion = reader.getStringElement();
+ } else if ("Value".equals(elementName.getLocalPart())) {
+ deserializedUserDelegationKey.value = reader.getStringElement();
+ } else {
+ reader.skipElement();
+ }
+ }
+
+ return deserializedUserDelegationKey;
+ });
+ }
+}
diff --git a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/sas/QueueServiceSasSignatureValues.java b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/sas/QueueServiceSasSignatureValues.java
index 9e2af2793155..dcece844a663 100644
--- a/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/sas/QueueServiceSasSignatureValues.java
+++ b/sdk/storage/azure-storage-queue/src/main/java/com/azure/storage/queue/sas/QueueServiceSasSignatureValues.java
@@ -28,18 +28,13 @@ public final class QueueServiceSasSignatureValues {
.get(Constants.PROPERTY_AZURE_STORAGE_SAS_SERVICE_VERSION, QueueServiceVersion.getLatest().getVersion());
private SasProtocol protocol;
-
private OffsetDateTime startTime;
-
private OffsetDateTime expiryTime;
-
private String permissions;
-
private SasIpRange sasIpRange;
-
private String queueName;
-
private String identifier;
+ private String delegatedUserObjectId;
/**
* Creates an object with empty values for all fields.
@@ -212,7 +207,7 @@ public QueueServiceSasSignatureValues setSasIpRange(SasIpRange sasIpRange) {
* Gets the name of the queue this SAS may access.
*
* @return The name of the queue the SAS user may access.
- * @deprecated Queue name is now auto-populated by the SAS generation methods provided on the desired queue client.
+ * @deprecated Queue name is now autopopulated by the SAS generation methods provided on the desired queue client.
*/
@Deprecated
public String getQueueName() {
@@ -225,7 +220,7 @@ public String getQueueName() {
* @param queueName Canonical name of the object the SAS grants access
* @return the updated QueueServiceSasSignatureValues object
* @deprecated Please use the generateSas methods provided on the desired queue client that will
- * auto-populate the queue name.
+ * autopopulate the queue name.
*/
@Deprecated
public QueueServiceSasSignatureValues setQueueName(String queueName) {
@@ -270,6 +265,30 @@ public QueueServiceSasSignatureValues setIdentifier(String identifier) {
return this;
}
+ /**
+ * Optional. Beginning in version 2025-07-05, this value specifies the Entra ID of the user that is authorized to
+ * use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been
+ * issued to the user specified in this value.
+ *
+ * @return The Entra ID of the user that is authorized to use the resulting SAS URL.
+ */
+ public String getDelegatedUserObjectId() {
+ return delegatedUserObjectId;
+ }
+
+ /**
+ * Optional. Beginning in version 2025-07-05, this value specifies the Entra ID of the user that is authorized to
+ * use the resulting SAS URL. The resulting SAS URL must be used in conjunction with an Entra ID token that has been
+ * issued to the user specified in this value.
+ *
+ * @param delegatedUserObjectId The Entra ID of the user that is authorized to use the resulting SAS URL.
+ * @return the updated QueueServiceSasSignatureValues object
+ */
+ public QueueServiceSasSignatureValues setDelegatedUserObjectId(String delegatedUserObjectId) {
+ this.delegatedUserObjectId = delegatedUserObjectId;
+ return this;
+ }
+
/**
* Uses an account's shared key credential to sign these signature values to produce the proper SAS query
* parameters.
diff --git a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueAsyncApiTests.java b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueAsyncApiTests.java
index 9d06fc7aa2ec..7e59683ec577 100644
--- a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueAsyncApiTests.java
+++ b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueAsyncApiTests.java
@@ -897,7 +897,7 @@ public void audienceFromString() {
@RequiredServiceVersion(clazz = QueueServiceVersion.class, min = "2025-07-05")
public void getSetAccessPolicyOAuth() {
// Arrange
- QueueServiceAsyncClient service = getOAuthQueueAsyncServiceClient();
+ QueueServiceAsyncClient service = getOAuthQueueServiceAsyncClient();
Mono createQueue = queueAsyncClient.createIfNotExists();
queueAsyncClient = service.getQueueAsyncClient(queueName);
diff --git a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueSasAsyncClientTests.java b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueSasAsyncClientTests.java
index 33884b813d1d..055c4ecbda46 100644
--- a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueSasAsyncClientTests.java
+++ b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueSasAsyncClientTests.java
@@ -3,26 +3,41 @@
package com.azure.storage.queue;
+import com.azure.core.credential.TokenCredential;
+import com.azure.core.http.rest.Response;
import com.azure.storage.common.sas.AccountSasPermission;
import com.azure.storage.common.sas.AccountSasResourceType;
import com.azure.storage.common.sas.AccountSasService;
import com.azure.storage.common.sas.AccountSasSignatureValues;
import com.azure.storage.common.sas.SasProtocol;
+import com.azure.storage.common.test.shared.StorageCommonTestUtils;
+import com.azure.storage.common.test.shared.extensions.LiveOnly;
+import com.azure.storage.common.test.shared.extensions.RequiredServiceVersion;
import com.azure.storage.queue.models.QueueAccessPolicy;
+import com.azure.storage.queue.models.QueueErrorCode;
+import com.azure.storage.queue.models.QueueMessageItem;
+import com.azure.storage.queue.models.QueueProperties;
import com.azure.storage.queue.models.QueueSignedIdentifier;
import com.azure.storage.queue.models.QueueStorageException;
import com.azure.storage.queue.models.SendMessageResult;
+import com.azure.storage.queue.models.UserDelegationKey;
import com.azure.storage.queue.sas.QueueSasPermission;
import com.azure.storage.queue.sas.QueueServiceSasSignatureValues;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
+import java.util.List;
+import static com.azure.storage.common.test.shared.StorageCommonTestUtils.getOidFromToken;
+import static com.azure.storage.queue.QueueTestHelper.assertExceptionStatusCodeAndMessage;
+import static com.azure.storage.queue.QueueTestHelper.assertResponseStatusCode;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -160,4 +175,113 @@ public void accountSasListQueues() {
assertDoesNotThrow(() -> sc.listQueues().next().block() != null);
}
+
+ // RBAC replication lag
+ @Test
+ @LiveOnly
+ @RequiredServiceVersion(clazz = QueueServiceVersion.class, min = "2026-02-06")
+ public void queueSasUserDelegationDelegatedObjectId() {
+ liveTestScenarioWithRetry(() -> {
+ QueueSasPermission permissions = new QueueSasPermission().setReadPermission(true);
+ OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1);
+
+ TokenCredential tokenCredential = StorageCommonTestUtils.getTokenCredential(interceptorManager);
+
+ // We need to get the object ID from the token credential used to authenticate the request
+ String oid = getOidFromToken(tokenCredential);
+ QueueServiceSasSignatureValues sasValues
+ = new QueueServiceSasSignatureValues(expiryTime, permissions).setDelegatedUserObjectId(oid);
+
+ Flux> response = getUserDelegationInfo().flatMapMany(key -> {
+ String sas = asyncSasClient.generateUserDelegationSas(sasValues, key);
+
+ // When a delegated user object ID is set, the client must be authenticated with both the SAS and the
+ // token credential.
+ QueueAsyncClient client = instrument(new QueueClientBuilder().endpoint(asyncSasClient.getQueueUrl())
+ .sasToken(sas)
+ .credential(tokenCredential)).buildAsyncClient();
+
+ return client.getPropertiesWithResponse();
+ });
+
+ StepVerifier.create(response).assertNext(r -> assertResponseStatusCode(r, 200)).verifyComplete();
+ });
+ }
+
+ // RBAC replication lag
+ @Test
+ @LiveOnly
+ @RequiredServiceVersion(clazz = QueueServiceVersion.class, min = "2026-02-06")
+ public void queueSasUserDelegationDelegatedObjectIdFail() {
+ liveTestScenarioWithRetry(() -> {
+ QueueSasPermission permissions = new QueueSasPermission().setReadPermission(true);
+ OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1);
+
+ TokenCredential tokenCredential = StorageCommonTestUtils.getTokenCredential(interceptorManager);
+
+ // We need to get the object ID from the token credential used to authenticate the request
+ String oid = getOidFromToken(tokenCredential);
+ QueueServiceSasSignatureValues sasValues
+ = new QueueServiceSasSignatureValues(expiryTime, permissions).setDelegatedUserObjectId(oid);
+
+ Flux> response = getUserDelegationInfo().flatMapMany(key -> {
+ String sas = asyncSasClient.generateUserDelegationSas(sasValues, key);
+
+ // When a delegated user object ID is set, the client must be authenticated with both the SAS and the
+ // token credential.
+ QueueAsyncClient client
+ = instrument(new QueueClientBuilder().endpoint(asyncSasClient.getQueueUrl()).sasToken(sas))
+ .buildAsyncClient();
+
+ return client.getPropertiesWithResponse();
+ });
+
+ StepVerifier.create(response)
+ .verifyErrorSatisfies(
+ e -> assertExceptionStatusCodeAndMessage(e, 403, QueueErrorCode.AUTHENTICATION_FAILED));
+ });
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = QueueServiceVersion.class, min = "2026-02-06")
+ public void sendMessageUserDelegationSAS() {
+ liveTestScenarioWithRetry(() -> {
+ QueueSasPermission permissions = new QueueSasPermission().setReadPermission(true)
+ .setAddPermission(true)
+ .setProcessPermission(true)
+ .setUpdatePermission(true);
+ OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1);
+
+ QueueServiceSasSignatureValues sasValues = new QueueServiceSasSignatureValues(expiryTime, permissions);
+
+ Mono> response = getUserDelegationInfo().flatMap(key -> {
+ String sas = asyncSasClient.generateUserDelegationSas(sasValues, key);
+
+ QueueAsyncClient client
+ = instrument(new QueueClientBuilder().endpoint(asyncSasClient.getQueueUrl()).sasToken(sas))
+ .buildAsyncClient();
+
+ return client.sendMessage(DATA.getDefaultBinaryData()).then(client.receiveMessages(2).collectList());
+ });
+
+ StepVerifier.create(response).assertNext(messageItemList -> {
+ // The first message is the one sent in setup.
+ assertEquals(2, messageItemList.size());
+ assertEquals("test", messageItemList.get(0).getBody().toString());
+ assertEquals(DATA.getDefaultText(), messageItemList.get(1).getBody().toString());
+ });
+ });
+ }
+
+ private Mono getUserDelegationInfo() {
+ return getOAuthServiceAsyncClient()
+ .getUserDelegationKey(testResourceNamer.now().minusDays(1), testResourceNamer.now().plusDays(1))
+ .flatMap(r -> {
+ String keyOid = testResourceNamer.recordValueFromConfig(r.getSignedObjectId());
+ r.setSignedObjectId(keyOid);
+ String keyTid = testResourceNamer.recordValueFromConfig(r.getSignedTenantId());
+ r.setSignedTenantId(keyTid);
+ return Mono.just(r);
+ });
+ }
}
diff --git a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueSasClientTests.java b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueSasClientTests.java
index d7b208bcc0c8..775f8849d517 100644
--- a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueSasClientTests.java
+++ b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueSasClientTests.java
@@ -3,16 +3,25 @@
package com.azure.storage.queue;
+import com.azure.core.credential.TokenCredential;
+import com.azure.core.http.rest.Response;
+import com.azure.core.util.Context;
import com.azure.storage.common.sas.AccountSasPermission;
import com.azure.storage.common.sas.AccountSasResourceType;
import com.azure.storage.common.sas.AccountSasService;
import com.azure.storage.common.sas.AccountSasSignatureValues;
import com.azure.storage.common.sas.SasProtocol;
+import com.azure.storage.common.test.shared.StorageCommonTestUtils;
+import com.azure.storage.common.test.shared.extensions.LiveOnly;
+import com.azure.storage.common.test.shared.extensions.RequiredServiceVersion;
import com.azure.storage.queue.models.QueueAccessPolicy;
+import com.azure.storage.queue.models.QueueErrorCode;
import com.azure.storage.queue.models.QueueMessageItem;
+import com.azure.storage.queue.models.QueueProperties;
import com.azure.storage.queue.models.QueueSignedIdentifier;
import com.azure.storage.queue.models.QueueStorageException;
import com.azure.storage.queue.models.SendMessageResult;
+import com.azure.storage.queue.models.UserDelegationKey;
import com.azure.storage.queue.sas.QueueSasPermission;
import com.azure.storage.queue.sas.QueueServiceSasSignatureValues;
import org.junit.jupiter.api.BeforeEach;
@@ -24,9 +33,14 @@
import java.util.Arrays;
import java.util.Iterator;
+import static com.azure.storage.common.test.shared.StorageCommonTestUtils.getOidFromToken;
+import static com.azure.storage.queue.QueueTestHelper.assertExceptionStatusCodeAndMessage;
+import static com.azure.storage.queue.QueueTestHelper.assertResponseStatusCode;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class QueueSasClientTests extends QueueTestBase {
private QueueClient sasClient;
@@ -172,4 +186,94 @@ public void rememberAboutStringToSignDeprecation() {
assertEquals(deprecatedStringToSign, client.generateSas(values));
}
+
+ // RBAC replication lag
+ @Test
+ @LiveOnly
+ @RequiredServiceVersion(clazz = QueueServiceVersion.class, min = "2026-02-06")
+ public void queueSasUserDelegationDelegatedObjectId() {
+ liveTestScenarioWithRetry(() -> {
+ QueueSasPermission permissions = new QueueSasPermission().setReadPermission(true);
+ OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1);
+
+ TokenCredential tokenCredential = StorageCommonTestUtils.getTokenCredential(interceptorManager);
+
+ // We need to get the object ID from the token credential used to authenticate the request
+ String oid = getOidFromToken(tokenCredential);
+ QueueServiceSasSignatureValues sasValues
+ = new QueueServiceSasSignatureValues(expiryTime, permissions).setDelegatedUserObjectId(oid);
+ String sas = sasClient.generateUserDelegationSas(sasValues, getUserDelegationInfo());
+
+ // When a delegated user object ID is set, the client must be authenticated with both the SAS and the
+ // token credential.
+ QueueClient client = instrument(
+ new QueueClientBuilder().endpoint(sasClient.getQueueUrl()).sasToken(sas).credential(tokenCredential))
+ .buildClient();
+
+ Response response = client.getPropertiesWithResponse(null, Context.NONE);
+ assertResponseStatusCode(response, 200);
+ });
+ }
+
+ // RBAC replication lag
+ @Test
+ @LiveOnly
+ @RequiredServiceVersion(clazz = QueueServiceVersion.class, min = "2026-02-06")
+ public void queueSasUserDelegationDelegatedObjectIdFail() {
+ liveTestScenarioWithRetry(() -> {
+ QueueSasPermission permissions = new QueueSasPermission().setReadPermission(true);
+ OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1);
+
+ TokenCredential tokenCredential = StorageCommonTestUtils.getTokenCredential(interceptorManager);
+
+ // We need to get the object ID from the token credential used to authenticate the request
+ String oid = getOidFromToken(tokenCredential);
+ QueueServiceSasSignatureValues sasValues
+ = new QueueServiceSasSignatureValues(expiryTime, permissions).setDelegatedUserObjectId(oid);
+ String sas = sasClient.generateUserDelegationSas(sasValues, getUserDelegationInfo());
+
+ // When a delegated user object ID is set, the client must be authenticated with both the SAS and the
+ // token credential. Token credential is not provided here, so the request should fail.
+ QueueClient client
+ = instrument(new QueueClientBuilder().endpoint(sasClient.getQueueUrl()).sasToken(sas)).buildClient();
+
+ QueueStorageException e
+ = assertThrows(QueueStorageException.class, () -> client.getPropertiesWithResponse(null, Context.NONE));
+ assertExceptionStatusCodeAndMessage(e, 403, QueueErrorCode.AUTHENTICATION_FAILED);
+ });
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = QueueServiceVersion.class, min = "2026-02-06")
+ public void sendMessageUserDelegationSAS() {
+ liveTestScenarioWithRetry(() -> {
+ QueueSasPermission permissions = new QueueSasPermission().setReadPermission(true)
+ .setAddPermission(true)
+ .setProcessPermission(true)
+ .setUpdatePermission(true);
+ OffsetDateTime expiryTime = testResourceNamer.now().plusHours(1);
+
+ QueueServiceSasSignatureValues sasValues = new QueueServiceSasSignatureValues(expiryTime, permissions);
+ String sas = sasClient.generateUserDelegationSas(sasValues, getUserDelegationInfo());
+
+ QueueClient client
+ = instrument(new QueueClientBuilder().endpoint(sasClient.getQueueUrl()).sasToken(sas)).buildClient();
+
+ client.sendMessage(DATA.getDefaultBinaryData());
+ Iterator dequeueMsgIter = client.receiveMessages(2).iterator();
+ assertTrue(dequeueMsgIter.hasNext());
+ dequeueMsgIter.next(); // Skip the first message, which is the one we sent in the setup
+ assertArrayEquals(DATA.getDefaultBytes(), dequeueMsgIter.next().getBody().toBytes());
+ });
+ }
+
+ protected UserDelegationKey getUserDelegationInfo() {
+ UserDelegationKey key = getOAuthServiceClient().getUserDelegationKey(testResourceNamer.now().minusDays(1),
+ testResourceNamer.now().plusDays(1));
+ String keyOid = testResourceNamer.recordValueFromConfig(key.getSignedObjectId());
+ key.setSignedObjectId(keyOid);
+ String keyTid = testResourceNamer.recordValueFromConfig(key.getSignedTenantId());
+ key.setSignedTenantId(keyTid);
+ return key;
+ }
}
diff --git a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueServiceApiTests.java b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueServiceApiTests.java
index 8e5901c27f1c..0c98ae65d2b8 100644
--- a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueServiceApiTests.java
+++ b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueServiceApiTests.java
@@ -6,6 +6,7 @@
import com.azure.core.http.rest.PagedIterable;
import com.azure.core.http.rest.PagedResponse;
import com.azure.core.http.rest.Response;
+import com.azure.core.util.Context;
import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.storage.common.test.shared.extensions.LiveOnly;
import com.azure.storage.common.test.shared.extensions.RequiredServiceVersion;
@@ -18,6 +19,7 @@
import com.azure.storage.queue.models.QueueServiceProperties;
import com.azure.storage.queue.models.QueueStorageException;
import com.azure.storage.queue.models.QueuesSegmentOptions;
+import com.azure.storage.queue.models.UserDelegationKey;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;
@@ -27,6 +29,8 @@
import java.net.MalformedURLException;
import java.net.URL;
+import java.time.OffsetDateTime;
+import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
@@ -272,4 +276,28 @@ public void audienceFromString() {
assertNotNull(aadService.getProperties());
}
+
+ @Test
+ @RequiredServiceVersion(clazz = QueueServiceVersion.class, min = "2026-02-06")
+ public void queueServiceGetUserDelegationKey() {
+ QueueServiceClient oAuthServiceClient = getOAuthQueueServiceClient();
+
+ OffsetDateTime expiry = testResourceNamer.now().plusHours(1).truncatedTo(ChronoUnit.SECONDS);
+ Response response
+ = oAuthServiceClient.getUserDelegationKeyWithResponse(testResourceNamer.now(), expiry, null, Context.NONE);
+
+ assertEquals(expiry, response.getValue().getSignedExpiry());
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = QueueServiceVersion.class, min = "2026-02-06")
+ public void queueServiceGetUserDelegationKeyAuthError() {
+ OffsetDateTime expiry = testResourceNamer.now().plusHours(1).truncatedTo(ChronoUnit.SECONDS);
+
+ //not oauth client
+ QueueStorageException e = assertThrows(QueueStorageException.class, () -> primaryQueueServiceClient
+ .getUserDelegationKeyWithResponse(testResourceNamer.now(), expiry, null, Context.NONE));
+
+ QueueTestHelper.assertExceptionStatusCodeAndMessage(e, 403, QueueErrorCode.AUTHENTICATION_FAILED);
+ }
}
diff --git a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueServiceAsyncApiTests.java b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueServiceAsyncApiTests.java
index b54015b65c24..ae16863899a8 100644
--- a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueServiceAsyncApiTests.java
+++ b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueServiceAsyncApiTests.java
@@ -15,6 +15,7 @@
import com.azure.storage.queue.models.QueueRetentionPolicy;
import com.azure.storage.queue.models.QueueServiceProperties;
import com.azure.storage.queue.models.QueuesSegmentOptions;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;
@@ -25,6 +26,8 @@
import java.net.MalformedURLException;
import java.net.URL;
+import java.time.OffsetDateTime;
+import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
@@ -235,7 +238,7 @@ public void audienceErrorBearerChallengeRetry() {
= getOAuthServiceClientBuilder().audience(QueueAudience.createQueueServiceAccountAudience("badaudience"))
.buildAsyncClient();
- StepVerifier.create(aadService.getProperties()).assertNext(r -> assertNotNull(r)).verifyComplete();
+ StepVerifier.create(aadService.getProperties()).assertNext(Assertions::assertNotNull).verifyComplete();
}
@Test
@@ -246,6 +249,29 @@ public void audienceFromString() {
QueueServiceAsyncClient aadService = getOAuthServiceClientBuilder().audience(audience).buildAsyncClient();
- StepVerifier.create(aadService.getProperties()).assertNext(r -> assertNotNull(r)).verifyComplete();
+ StepVerifier.create(aadService.getProperties()).assertNext(Assertions::assertNotNull).verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = QueueServiceVersion.class, min = "2026-02-06")
+ public void queueServiceGetUserDelegationKey() {
+ QueueServiceAsyncClient oAuthServiceClient = getOAuthQueueServiceAsyncClient();
+
+ OffsetDateTime expiry = testResourceNamer.now().plusHours(1).truncatedTo(ChronoUnit.SECONDS);
+ StepVerifier.create(oAuthServiceClient.getUserDelegationKeyWithResponse(testResourceNamer.now(), expiry))
+ .assertNext(r -> assertEquals(expiry, r.getValue().getSignedExpiry()))
+ .verifyComplete();
+ }
+
+ @Test
+ @RequiredServiceVersion(clazz = QueueServiceVersion.class, min = "2026-02-06")
+ public void queueServiceGetUserDelegationKeyAuthError() {
+ OffsetDateTime expiry = testResourceNamer.now().plusHours(1).truncatedTo(ChronoUnit.SECONDS);
+
+ //not oauth client
+ StepVerifier
+ .create(primaryQueueServiceAsyncClient.getUserDelegationKeyWithResponse(testResourceNamer.now(), expiry))
+ .verifyErrorSatisfies(
+ e -> QueueTestHelper.assertExceptionStatusCodeAndMessage(e, 403, QueueErrorCode.AUTHENTICATION_FAILED));
}
}
diff --git a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueTestBase.java b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueTestBase.java
index 23b94a8041ca..5b785661b5b8 100644
--- a/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueTestBase.java
+++ b/sdk/storage/azure-storage-queue/src/test/java/com/azure/storage/queue/QueueTestBase.java
@@ -13,6 +13,7 @@
import com.azure.core.util.Context;
import com.azure.storage.common.StorageSharedKeyCredential;
import com.azure.storage.common.test.shared.StorageCommonTestUtils;
+import com.azure.storage.common.test.shared.TestDataFactory;
import com.azure.storage.common.test.shared.TestEnvironment;
import com.azure.storage.common.test.shared.policy.PerCallVersionPolicy;
import com.azure.storage.queue.models.QueuesSegmentOptions;
@@ -26,6 +27,7 @@
*/
public class QueueTestBase extends TestProxyTestBase {
protected static final TestEnvironment ENVIRONMENT = TestEnvironment.getInstance();
+ protected static final TestDataFactory DATA = TestDataFactory.getInstance();
protected String prefix;
@@ -88,7 +90,7 @@ protected QueueServiceClient getOAuthQueueServiceClient() {
return getOAuthServiceClientBuilder().buildClient();
}
- protected QueueServiceAsyncClient getOAuthQueueAsyncServiceClient() {
+ protected QueueServiceAsyncClient getOAuthQueueServiceAsyncClient() {
return getOAuthServiceClientBuilder().buildAsyncClient();
}
@@ -145,4 +147,44 @@ protected , E extends Enum> T instrument(T builder) {
protected String getPrimaryConnectionString() {
return ENVIRONMENT.getPrimaryAccount().getConnectionString();
}
+
+ protected void liveTestScenarioWithRetry(Runnable runnable) {
+ if (!interceptorManager.isLiveMode()) {
+ runnable.run();
+ return;
+ }
+
+ int retry = 0;
+
+ // Try up to 4 times
+ while (retry < 4) {
+ try {
+ runnable.run();
+ return; // success
+ } catch (Exception ex) {
+ retry++;
+ sleepIfRunningAgainstService(5000);
+ }
+ }
+ // Final attempt (5th try)
+ runnable.run();
+ }
+
+ protected QueueServiceClient getOAuthServiceClient() {
+ QueueServiceClientBuilder builder
+ = new QueueServiceClientBuilder().endpoint(ENVIRONMENT.getPrimaryAccount().getQueueEndpoint());
+
+ instrument(builder);
+
+ return builder.credential(StorageCommonTestUtils.getTokenCredential(interceptorManager)).buildClient();
+ }
+
+ protected QueueServiceAsyncClient getOAuthServiceAsyncClient() {
+ QueueServiceClientBuilder builder
+ = new QueueServiceClientBuilder().endpoint(ENVIRONMENT.getPrimaryAccount().getQueueEndpoint());
+
+ instrument(builder);
+
+ return builder.credential(StorageCommonTestUtils.getTokenCredential(interceptorManager)).buildAsyncClient();
+ }
}
diff --git a/sdk/storage/azure-storage-queue/swagger/README.md b/sdk/storage/azure-storage-queue/swagger/README.md
index 99294513c115..ab3d56748c6f 100644
--- a/sdk/storage/azure-storage-queue/swagger/README.md
+++ b/sdk/storage/azure-storage-queue/swagger/README.md
@@ -15,7 +15,7 @@ autorest
### Code generation settings
``` yaml
use: '@autorest/java@4.1.52'
-input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/f031b8ef2830772c5c33d1768886551f7e538b7c/specification/storage/data-plane/Microsoft.QueueStorage/stable/2018-03-28/queue.json
+input-file: https://raw.githubusercontent.com/seanmcc-msft/azure-rest-api-specs/422c0661faac7c93463987d6437c6778818718f5/specification/storage/data-plane/Microsoft.QueueStorage/stable/2026-02-06/queue.json
java: true
output-folder: ../
namespace: com.azure.storage.queue
@@ -24,7 +24,7 @@ license-header: MICROSOFT_MIT_SMALL
enable-sync-stack: true
default-http-exception-type: com.azure.storage.queue.implementation.models.QueueStorageExceptionInternal
models-subpackage: implementation.models
-custom-types: QueueErrorCode,QueueSignedIdentifier,SendMessageResult,QueueMessageItem,PeekedMessageItem,QueueItem,QueueServiceProperties,QueueServiceStatistics,QueueCorsRule,QueueAccessPolicy,QueueAnalyticsLogging,QueueMetrics,QueueRetentionPolicy,GeoReplicationStatus,GeoReplicationStatusType,GeoReplication
+custom-types: QueueErrorCode,QueueSignedIdentifier,SendMessageResult,QueueMessageItem,PeekedMessageItem,QueueItem,QueueServiceProperties,QueueServiceStatistics,QueueCorsRule,QueueAccessPolicy,QueueAnalyticsLogging,QueueMetrics,QueueRetentionPolicy,GeoReplicationStatus,GeoReplicationStatusType,GeoReplication,UserDelegationKey,KeyInfo
custom-types-subpackage: models
customization-class: src/main/java/QueueStorageCustomization.java
use-input-stream-for-binary: true
@@ -141,4 +141,14 @@ directive:
$["x-ms-pageable"].itemName = "QueueItems";
```
+### Rename UserDelegationKey SignedOid and SignedTid
+``` yaml
+directive:
+- from: swagger-document
+ where: $.definitions.UserDelegationKey
+ transform: >
+ $.properties.SignedOid["x-ms-client-name"] = "signedObjectId";
+ $.properties.SignedTid["x-ms-client-name"] = "signedTenantId";
+```
+