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"; +``` +