diff --git a/sdk/storage/azure-storage-blob/assets.json b/sdk/storage/azure-storage-blob/assets.json index 8cad139f33ff..9b406d7acb7b 100644 --- a/sdk/storage/azure-storage-blob/assets.json +++ b/sdk/storage/azure-storage-blob/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/storage/azure-storage-blob", - "Tag": "java/storage/azure-storage-blob_47f4243e59" + "Tag": "java/storage/azure-storage-blob_63fe5a46f1" } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java index b86fe4e76b2f..c1ad39c451b7 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerAsyncClient.java @@ -28,6 +28,9 @@ import com.azure.storage.blob.implementation.models.EncryptionScope; import com.azure.storage.blob.implementation.models.ListBlobsFlatSegmentResponse; import com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse; +import com.azure.storage.blob.implementation.models.AuthenticationType; +import com.azure.storage.blob.implementation.models.CreateSessionConfiguration; +import com.azure.storage.blob.implementation.models.CreateSessionResponse; import com.azure.storage.blob.implementation.util.BlobConstants; import com.azure.storage.blob.implementation.util.BlobSasImplUtil; import com.azure.storage.blob.implementation.util.ModelHelper; @@ -1691,11 +1694,39 @@ public String generateSas(BlobServiceSasSignatureValues blobServiceSasSignatureV .generateSas(SasImplUtils.extractSharedKeyCredential(getHttpPipeline()), stringToSignHandler, context); } - // private boolean validateNoTime(BlobRequestConditions modifiedRequestConditions) { - // if (modifiedRequestConditions == null) { - // return true; - // } - // return modifiedRequestConditions.getIfModifiedSince() == null - // && modifiedRequestConditions.getIfUnmodifiedSince() == null; - // } + /** + * Creates a session scoped to this container. The session provides temporary credentials (a session token and + * session key) that can be used to sign subsequent requests using the Shared Key protocol. + * + * @return A {@link Mono} containing the {@link CreateSessionResponse} with session credentials. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public Mono createSession() { + return createSessionWithResponse().flatMap(FluxUtil::toMono); + } + + /** + * Creates a session scoped to this container. The session provides temporary credentials (a session token and + * session key) that can be used to sign subsequent requests using the Shared Key protocol. + * + * @return A {@link Mono} containing a {@link Response} with the {@link CreateSessionResponse}. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public Mono> createSessionWithResponse() { + try { + return withContext(this::createSessionWithResponse); + } catch (RuntimeException ex) { + return monoError(LOGGER, ex); + } + } + + Mono> createSessionWithResponse(Context context) { + context = context == null ? Context.NONE : context; + CreateSessionConfiguration config + = new CreateSessionConfiguration().setAuthenticationType(AuthenticationType.HMAC); + return this.azureBlobStorage.getContainers() + .createSessionWithResponseAsync(containerName, config, context) + .map(response -> new SimpleResponse<>(response, response.getValue())); + } + } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java index 64de81617f9c..2331530ad3cd 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobContainerClient.java @@ -31,6 +31,9 @@ import com.azure.storage.blob.implementation.models.FilterBlobSegment; import com.azure.storage.blob.implementation.models.ListBlobsFlatSegmentResponse; import com.azure.storage.blob.implementation.models.ListBlobsHierarchySegmentResponse; +import com.azure.storage.blob.implementation.models.AuthenticationType; +import com.azure.storage.blob.implementation.models.CreateSessionConfiguration; +import com.azure.storage.blob.implementation.models.CreateSessionResponse; import com.azure.storage.blob.implementation.util.BlobConstants; import com.azure.storage.blob.implementation.util.BlobSasImplUtil; import com.azure.storage.blob.implementation.util.ModelHelper; @@ -1509,4 +1512,33 @@ public String generateSas(BlobServiceSasSignatureValues blobServiceSasSignatureV .generateSas(SasImplUtils.extractSharedKeyCredential(getHttpPipeline()), stringToSignHandler, context); } + /** + * Creates a session scoped to this container. The session provides temporary credentials (a session token and + * session key) that can be used to sign subsequent requests using the Shared Key protocol. + * + * @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 response containing the {@link CreateSessionResponse} with session credentials. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public Response createSessionWithResponse(Duration timeout, Context context) { + Context finalContext = context == null ? Context.NONE : context; + CreateSessionConfiguration config + = new CreateSessionConfiguration().setAuthenticationType(AuthenticationType.HMAC); + Callable> operation = () -> this.azureBlobStorage.getContainers() + .createSessionWithResponse(containerName, config, null, null, finalContext); + return sendRequest(operation, timeout, BlobStorageException.class); + } + + /** + * Creates a session scoped to this container. The session provides temporary credentials (a session token and + * session key) that can be used to sign subsequent requests using the Shared Key protocol. + * + * @return The {@link CreateSessionResponse} with session credentials. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public CreateSessionResponse createSession() { + return createSessionWithResponse(null, Context.NONE).getValue(); + } + } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceVersion.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceVersion.java index 75fb74a59a5d..efa24e69b84d 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceVersion.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/BlobServiceVersion.java @@ -157,12 +157,7 @@ public enum BlobServiceVersion implements ServiceVersion { /** * Service version {@code 2026-04-06}. */ - V2026_04_06("2026-04-06"), - - /** - * Service version {@code 2026-06-06}. - */ - V2026_06_06("2026-06-06"); + V2026_04_06("2026-04-06"); private final String version; @@ -184,6 +179,6 @@ public String getVersion() { * @return the latest {@link BlobServiceVersion} */ public static BlobServiceVersion getLatest() { - return V2026_06_06; + return V2026_04_06; } } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/ContainersImpl.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/ContainersImpl.java index 7fd2af96e4df..8bc27e750abc 100644 --- a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/ContainersImpl.java +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/ContainersImpl.java @@ -46,6 +46,8 @@ import com.azure.storage.blob.implementation.models.ContainersSetAccessPolicyHeaders; import com.azure.storage.blob.implementation.models.ContainersSetMetadataHeaders; import com.azure.storage.blob.implementation.models.ContainersSubmitBatchHeaders; +import com.azure.storage.blob.implementation.models.CreateSessionConfiguration; +import com.azure.storage.blob.implementation.models.CreateSessionResponse; import com.azure.storage.blob.implementation.models.FilterBlobSegment; import com.azure.storage.blob.implementation.models.FilterBlobsIncludeItem; import com.azure.storage.blob.implementation.models.ListBlobsFlatSegmentResponse; @@ -938,6 +940,26 @@ Response getAccountInfoNoCustomHeadersSync(@HostParam("url") String url, @QueryParam("comp") String comp, @QueryParam("timeout") Integer timeout, @HeaderParam("x-ms-version") String version, @HeaderParam("x-ms-client-request-id") String requestId, @HeaderParam("Accept") String accept, Context context); + + @Post("/{containerName}") + @ExpectedResponses({ 201 }) + @UnexpectedResponseExceptionType(BlobStorageExceptionInternal.class) + Mono> createSession(@HostParam("url") String url, + @PathParam("containerName") String containerName, @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") CreateSessionConfiguration createSessionConfiguration, + @HeaderParam("Accept") String accept, Context context); + + @Post("/{containerName}") + @ExpectedResponses({ 201 }) + @UnexpectedResponseExceptionType(BlobStorageExceptionInternal.class) + Response createSessionSync(@HostParam("url") String url, + @PathParam("containerName") String containerName, @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") CreateSessionConfiguration createSessionConfiguration, + @HeaderParam("Accept") String accept, Context context); } /** @@ -6707,4 +6729,159 @@ public Response getAccountInfoNoCustomHeadersWithResponse(String container throw ModelHelper.mapToBlobStorageException(internalException); } } + + /** + * The Create Session operation enables users to create a session scoped to a container. + * + * @param containerName The container name. + * @param createSessionConfiguration The createSessionConfiguration parameter. + * @param timeout The timeout parameter is expressed in seconds. For more information, see <a + * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-blob-service-operations">Setting + * Timeouts for Blob 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 BlobStorageExceptionInternal thrown if the request is rejected by server. + * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent. + * @return the response body along with {@link Response} on successful completion of {@link Mono}. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public Mono> createSessionWithResponseAsync(String containerName, + CreateSessionConfiguration createSessionConfiguration, Integer timeout, String requestId) { + return FluxUtil + .withContext(context -> createSessionWithResponseAsync(containerName, createSessionConfiguration, timeout, + requestId, context)) + .onErrorMap(BlobStorageExceptionInternal.class, ModelHelper::mapToBlobStorageException); + } + + /** + * The Create Session operation enables users to create a session scoped to a container. + * + * @param containerName The container name. + * @param createSessionConfiguration The createSessionConfiguration parameter. + * @param timeout The timeout parameter is expressed in seconds. For more information, see <a + * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-blob-service-operations">Setting + * Timeouts for Blob 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 BlobStorageExceptionInternal thrown if the request is rejected by server. + * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent. + * @return the response body along with {@link Response} on successful completion of {@link Mono}. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public Mono> createSessionWithResponseAsync(String containerName, + CreateSessionConfiguration createSessionConfiguration, Integer timeout, String requestId, Context context) { + final String restype = "container"; + final String comp = "session"; + final String accept = "application/xml"; + return service + .createSession(this.client.getUrl(), containerName, restype, comp, timeout, this.client.getVersion(), + requestId, createSessionConfiguration, accept, context) + .onErrorMap(BlobStorageExceptionInternal.class, ModelHelper::mapToBlobStorageException); + } + + /** + * The Create Session operation enables users to create a session scoped to a container. + * + * @param containerName The container name. + * @param createSessionConfiguration The createSessionConfiguration parameter. + * @param timeout The timeout parameter is expressed in seconds. For more information, see <a + * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-blob-service-operations">Setting + * Timeouts for Blob 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 BlobStorageExceptionInternal thrown if the request is rejected by server. + * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent. + * @return the response body on successful completion of {@link Mono}. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public Mono createSessionAsync(String containerName, + CreateSessionConfiguration createSessionConfiguration, Integer timeout, String requestId) { + return createSessionWithResponseAsync(containerName, createSessionConfiguration, timeout, requestId) + .onErrorMap(BlobStorageExceptionInternal.class, ModelHelper::mapToBlobStorageException) + .flatMap(res -> Mono.justOrEmpty(res.getValue())); + } + + /** + * The Create Session operation enables users to create a session scoped to a container. + * + * @param containerName The container name. + * @param createSessionConfiguration The createSessionConfiguration parameter. + * @param timeout The timeout parameter is expressed in seconds. For more information, see <a + * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-blob-service-operations">Setting + * Timeouts for Blob 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 BlobStorageExceptionInternal thrown if the request is rejected by server. + * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent. + * @return the response body on successful completion of {@link Mono}. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public Mono createSessionAsync(String containerName, + CreateSessionConfiguration createSessionConfiguration, Integer timeout, String requestId, Context context) { + return createSessionWithResponseAsync(containerName, createSessionConfiguration, timeout, requestId, context) + .onErrorMap(BlobStorageExceptionInternal.class, ModelHelper::mapToBlobStorageException) + .flatMap(res -> Mono.justOrEmpty(res.getValue())); + } + + /** + * The Create Session operation enables users to create a session scoped to a container. + * + * @param containerName The container name. + * @param createSessionConfiguration The createSessionConfiguration parameter. + * @param timeout The timeout parameter is expressed in seconds. For more information, see <a + * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-blob-service-operations">Setting + * Timeouts for Blob 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 BlobStorageExceptionInternal thrown if the request is rejected by server. + * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent. + * @return the response body along with {@link Response}. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public Response createSessionWithResponse(String containerName, + CreateSessionConfiguration createSessionConfiguration, Integer timeout, String requestId, Context context) { + try { + final String restype = "container"; + final String comp = "session"; + final String accept = "application/xml"; + return service.createSessionSync(this.client.getUrl(), containerName, restype, comp, timeout, + this.client.getVersion(), requestId, createSessionConfiguration, accept, context); + } catch (BlobStorageExceptionInternal internalException) { + throw ModelHelper.mapToBlobStorageException(internalException); + } + } + + /** + * The Create Session operation enables users to create a session scoped to a container. + * + * @param containerName The container name. + * @param createSessionConfiguration The createSessionConfiguration parameter. + * @param timeout The timeout parameter is expressed in seconds. For more information, see <a + * href="https://learn.microsoft.com/rest/api/storageservices/setting-timeouts-for-blob-service-operations">Setting + * Timeouts for Blob 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 BlobStorageExceptionInternal thrown if the request is rejected by server. + * @throws RuntimeException all other wrapped checked exceptions if the request fails to be sent. + * @return the response. + */ + @ServiceMethod(returns = ReturnType.SINGLE) + public CreateSessionResponse createSession(String containerName, + CreateSessionConfiguration createSessionConfiguration, Integer timeout, String requestId) { + try { + return createSessionWithResponse(containerName, createSessionConfiguration, timeout, requestId, + Context.NONE).getValue(); + } catch (BlobStorageExceptionInternal internalException) { + throw ModelHelper.mapToBlobStorageException(internalException); + } + } } diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/AuthenticationType.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/AuthenticationType.java new file mode 100644 index 000000000000..76a92bba45e3 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/AuthenticationType.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) AutoRest Code Generator. + +package com.azure.storage.blob.implementation.models; + +import com.azure.core.annotation.Generated; +import com.azure.core.util.ExpandableStringEnum; +import java.util.Collection; + +/** + * The type of authentication required to create the session. The only type currently supported is HMAC. + */ +public final class AuthenticationType extends ExpandableStringEnum { + /** + * Static value HMAC for AuthenticationType. + */ + @Generated + public static final AuthenticationType HMAC = fromString("HMAC"); + + /** + * Creates a new instance of AuthenticationType value. + * + * @deprecated Use the {@link #fromString(String)} factory method. + */ + @Generated + @Deprecated + public AuthenticationType() { + } + + /** + * Creates or finds a AuthenticationType from its string representation. + * + * @param name a name to look for. + * @return the corresponding AuthenticationType. + */ + @Generated + public static AuthenticationType fromString(String name) { + return fromString(name, AuthenticationType.class); + } + + /** + * Gets known AuthenticationType values. + * + * @return known AuthenticationType values. + */ + @Generated + public static Collection values() { + return values(AuthenticationType.class); + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/CreateSessionConfiguration.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/CreateSessionConfiguration.java new file mode 100644 index 000000000000..b52e86f169cd --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/CreateSessionConfiguration.java @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) AutoRest Code Generator. + +package com.azure.storage.blob.implementation.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; + +/** + * The CreateSessionConfiguration model. + */ +@Fluent +public final class CreateSessionConfiguration implements XmlSerializable { + /* + * The type of authentication required to create the session. The only type currently supported is HMAC. + */ + @Generated + private AuthenticationType authenticationType; + + /** + * Creates an instance of CreateSessionConfiguration class. + */ + @Generated + public CreateSessionConfiguration() { + } + + /** + * Get the authenticationType property: The type of authentication required to create the session. The only type + * currently supported is HMAC. + * + * @return the authenticationType value. + */ + @Generated + public AuthenticationType getAuthenticationType() { + return this.authenticationType; + } + + /** + * Set the authenticationType property: The type of authentication required to create the session. The only type + * currently supported is HMAC. + * + * @param authenticationType the authenticationType value to set. + * @return the CreateSessionConfiguration object itself. + */ + @Generated + public CreateSessionConfiguration setAuthenticationType(AuthenticationType authenticationType) { + this.authenticationType = authenticationType; + 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() ? "CreateSessionRequest" : rootElementName; + xmlWriter.writeStartElement(rootElementName); + xmlWriter.writeStringElement("AuthenticationType", + this.authenticationType == null ? null : this.authenticationType.toString()); + return xmlWriter.writeEndElement(); + } + + /** + * Reads an instance of CreateSessionConfiguration from the XmlReader. + * + * @param xmlReader The XmlReader being read. + * @return An instance of CreateSessionConfiguration 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 CreateSessionConfiguration. + */ + @Generated + public static CreateSessionConfiguration fromXml(XmlReader xmlReader) throws XMLStreamException { + return fromXml(xmlReader, null); + } + + /** + * Reads an instance of CreateSessionConfiguration 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 CreateSessionConfiguration 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 CreateSessionConfiguration. + */ + @Generated + public static CreateSessionConfiguration fromXml(XmlReader xmlReader, String rootElementName) + throws XMLStreamException { + String finalRootElementName + = rootElementName == null || rootElementName.isEmpty() ? "CreateSessionRequest" : rootElementName; + return xmlReader.readObject(finalRootElementName, reader -> { + CreateSessionConfiguration deserializedCreateSessionConfiguration = new CreateSessionConfiguration(); + while (reader.nextElement() != XmlToken.END_ELEMENT) { + QName elementName = reader.getElementName(); + + if ("AuthenticationType".equals(elementName.getLocalPart())) { + deserializedCreateSessionConfiguration.authenticationType + = AuthenticationType.fromString(reader.getStringElement()); + } else { + reader.skipElement(); + } + } + + return deserializedCreateSessionConfiguration; + }); + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/CreateSessionResponse.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/CreateSessionResponse.java new file mode 100644 index 000000000000..610080c98fd4 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/CreateSessionResponse.java @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) AutoRest Code Generator. + +package com.azure.storage.blob.implementation.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.Generated; +import com.azure.core.util.DateTimeRfc1123; +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.util.Objects; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLStreamException; + +/** + * The CreateSessionResponse model. + */ +@Fluent +public final class CreateSessionResponse implements XmlSerializable { + /* + * A unique identifier for the created session. + */ + @Generated + private String id; + + /* + * The time when the session will expire. The format follows RFC 1123. + */ + @Generated + private DateTimeRfc1123 expiration; + + /* + * The type of authentication required to create the session. The only type currently supported is HMAC. + */ + @Generated + private AuthenticationType authenticationType; + + /* + * The Credentials property. + */ + @Generated + private SessionCredentials credentials; + + /** + * Creates an instance of CreateSessionResponse class. + */ + @Generated + public CreateSessionResponse() { + } + + /** + * Get the id property: A unique identifier for the created session. + * + * @return the id value. + */ + @Generated + public String getId() { + return this.id; + } + + /** + * Set the id property: A unique identifier for the created session. + * + * @param id the id value to set. + * @return the CreateSessionResponse object itself. + */ + @Generated + public CreateSessionResponse setId(String id) { + this.id = id; + return this; + } + + /** + * Get the expiration property: The time when the session will expire. The format follows RFC 1123. + * + * @return the expiration value. + */ + @Generated + public OffsetDateTime getExpiration() { + if (this.expiration == null) { + return null; + } + return this.expiration.getDateTime(); + } + + /** + * Set the expiration property: The time when the session will expire. The format follows RFC 1123. + * + * @param expiration the expiration value to set. + * @return the CreateSessionResponse object itself. + */ + @Generated + public CreateSessionResponse setExpiration(OffsetDateTime expiration) { + if (expiration == null) { + this.expiration = null; + } else { + this.expiration = new DateTimeRfc1123(expiration); + } + return this; + } + + /** + * Get the authenticationType property: The type of authentication required to create the session. The only type + * currently supported is HMAC. + * + * @return the authenticationType value. + */ + @Generated + public AuthenticationType getAuthenticationType() { + return this.authenticationType; + } + + /** + * Set the authenticationType property: The type of authentication required to create the session. The only type + * currently supported is HMAC. + * + * @param authenticationType the authenticationType value to set. + * @return the CreateSessionResponse object itself. + */ + @Generated + public CreateSessionResponse setAuthenticationType(AuthenticationType authenticationType) { + this.authenticationType = authenticationType; + return this; + } + + /** + * Get the credentials property: The Credentials property. + * + * @return the credentials value. + */ + @Generated + public SessionCredentials getCredentials() { + return this.credentials; + } + + /** + * Set the credentials property: The Credentials property. + * + * @param credentials the credentials value to set. + * @return the CreateSessionResponse object itself. + */ + @Generated + public CreateSessionResponse setCredentials(SessionCredentials credentials) { + this.credentials = credentials; + 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() ? "CreateSessionResult" : rootElementName; + xmlWriter.writeStartElement(rootElementName); + xmlWriter.writeStringElement("Id", this.id); + xmlWriter.writeStringElement("Expiration", Objects.toString(this.expiration, null)); + xmlWriter.writeStringElement("AuthenticationType", + this.authenticationType == null ? null : this.authenticationType.toString()); + xmlWriter.writeXml(this.credentials, "Credentials"); + return xmlWriter.writeEndElement(); + } + + /** + * Reads an instance of CreateSessionResponse from the XmlReader. + * + * @param xmlReader The XmlReader being read. + * @return An instance of CreateSessionResponse 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 CreateSessionResponse. + */ + @Generated + public static CreateSessionResponse fromXml(XmlReader xmlReader) throws XMLStreamException { + return fromXml(xmlReader, null); + } + + /** + * Reads an instance of CreateSessionResponse 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 CreateSessionResponse 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 CreateSessionResponse. + */ + @Generated + public static CreateSessionResponse fromXml(XmlReader xmlReader, String rootElementName) throws XMLStreamException { + String finalRootElementName + = rootElementName == null || rootElementName.isEmpty() ? "CreateSessionResult" : rootElementName; + return xmlReader.readObject(finalRootElementName, reader -> { + CreateSessionResponse deserializedCreateSessionResponse = new CreateSessionResponse(); + while (reader.nextElement() != XmlToken.END_ELEMENT) { + QName elementName = reader.getElementName(); + + if ("Id".equals(elementName.getLocalPart())) { + deserializedCreateSessionResponse.id = reader.getStringElement(); + } else if ("Expiration".equals(elementName.getLocalPart())) { + deserializedCreateSessionResponse.expiration = reader.getNullableElement(DateTimeRfc1123::new); + } else if ("AuthenticationType".equals(elementName.getLocalPart())) { + deserializedCreateSessionResponse.authenticationType + = AuthenticationType.fromString(reader.getStringElement()); + } else if ("Credentials".equals(elementName.getLocalPart())) { + deserializedCreateSessionResponse.credentials = SessionCredentials.fromXml(reader, "Credentials"); + } else { + reader.skipElement(); + } + } + + return deserializedCreateSessionResponse; + }); + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/SessionCredentials.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/SessionCredentials.java new file mode 100644 index 000000000000..ed427a221aa7 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/models/SessionCredentials.java @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) AutoRest Code Generator. + +package com.azure.storage.blob.implementation.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; + +/** + * The SessionCredentials model. + */ +@Fluent +public final class SessionCredentials implements XmlSerializable { + /* + * An opaque token used to authorize subsequent requests in the session. Must be treated as a security credential. + */ + @Generated + private String sessionToken; + + /* + * Only returned when AuthenticationType is HMAC. A symmetric encryption key used to sign requests in the session + * using the Shared Key protocol. + */ + @Generated + private String sessionKey; + + /** + * Creates an instance of SessionCredentials class. + */ + @Generated + public SessionCredentials() { + } + + /** + * Get the sessionToken property: An opaque token used to authorize subsequent requests in the session. Must be + * treated as a security credential. + * + * @return the sessionToken value. + */ + @Generated + public String getSessionToken() { + return this.sessionToken; + } + + /** + * Set the sessionToken property: An opaque token used to authorize subsequent requests in the session. Must be + * treated as a security credential. + * + * @param sessionToken the sessionToken value to set. + * @return the SessionCredentials object itself. + */ + @Generated + public SessionCredentials setSessionToken(String sessionToken) { + this.sessionToken = sessionToken; + return this; + } + + /** + * Get the sessionKey property: Only returned when AuthenticationType is HMAC. A symmetric encryption key used to + * sign requests in the session using the Shared Key protocol. + * + * @return the sessionKey value. + */ + @Generated + public String getSessionKey() { + return this.sessionKey; + } + + /** + * Set the sessionKey property: Only returned when AuthenticationType is HMAC. A symmetric encryption key used to + * sign requests in the session using the Shared Key protocol. + * + * @param sessionKey the sessionKey value to set. + * @return the SessionCredentials object itself. + */ + @Generated + public SessionCredentials setSessionKey(String sessionKey) { + this.sessionKey = sessionKey; + 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() ? "Credentials" : rootElementName; + xmlWriter.writeStartElement(rootElementName); + xmlWriter.writeStringElement("SessionToken", this.sessionToken); + xmlWriter.writeStringElement("SessionKey", this.sessionKey); + return xmlWriter.writeEndElement(); + } + + /** + * Reads an instance of SessionCredentials from the XmlReader. + * + * @param xmlReader The XmlReader being read. + * @return An instance of SessionCredentials 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 SessionCredentials. + */ + @Generated + public static SessionCredentials fromXml(XmlReader xmlReader) throws XMLStreamException { + return fromXml(xmlReader, null); + } + + /** + * Reads an instance of SessionCredentials 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 SessionCredentials 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 SessionCredentials. + */ + @Generated + public static SessionCredentials fromXml(XmlReader xmlReader, String rootElementName) throws XMLStreamException { + String finalRootElementName + = rootElementName == null || rootElementName.isEmpty() ? "Credentials" : rootElementName; + return xmlReader.readObject(finalRootElementName, reader -> { + SessionCredentials deserializedSessionCredentials = new SessionCredentials(); + while (reader.nextElement() != XmlToken.END_ELEMENT) { + QName elementName = reader.getElementName(); + + if ("SessionToken".equals(elementName.getLocalPart())) { + deserializedSessionCredentials.sessionToken = reader.getStringElement(); + } else if ("SessionKey".equals(elementName.getLocalPart())) { + deserializedSessionCredentials.sessionKey = reader.getStringElement(); + } else { + reader.skipElement(); + } + } + + return deserializedSessionCredentials; + }); + } +} diff --git a/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/StorageSessionCredential.java b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/StorageSessionCredential.java new file mode 100644 index 000000000000..a5fb88af89ee --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/main/java/com/azure/storage/blob/implementation/util/StorageSessionCredential.java @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob.implementation.util; + +import com.azure.core.http.HttpHeader; +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpHeaders; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.Header; +import com.azure.storage.common.implementation.StorageImplUtils; + +import java.net.URL; +import java.text.Collator; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; + +import static com.azure.storage.common.Utility.urlDecode; + +/** + * Holds session credentials (token, key, expiration) and signs requests using the Shared Key protocol. + * The Authorization header format is: {@code Session :} + */ +public final class StorageSessionCredential { + + private static final HttpHeaderName X_MS_DATE = HttpHeaderName.fromString("x-ms-date"); + + private final String sessionToken; + private final String sessionKey; + private final OffsetDateTime expiration; + + /** + * Creates a StorageSessionCredential with the given session token, key, and expiration. + * + * @param sessionToken The opaque session token from Create Session response. + * @param sessionKey The Base64-encoded symmetric key for HMAC signing. + * @param expiration The time when this session expires. + */ + public StorageSessionCredential(String sessionToken, String sessionKey, OffsetDateTime expiration) { + this.sessionToken = sessionToken; + this.sessionKey = sessionKey; + this.expiration = expiration; + } + + /** + * Computes an HMAC-SHA256 signature for the given string-to-sign using the session key. + * + * @param stringToSign The string to sign. + * @return The Base64-encoded HMAC-SHA256 signature. + */ + public String computeHmac256(String stringToSign) { + return StorageImplUtils.computeHMac256(sessionKey, stringToSign); + } + + /** + * Generates the Session Authorization header value for a request. + * Format: {@code Session :} + * + * @param requestURL The request URL. + * @param httpMethod The HTTP method (GET, PUT, etc.). + * @param headers The request headers. + * @return The Authorization header value. + */ + public String generateAuthorizationHeader(URL requestURL, String httpMethod, HttpHeaders headers) { + String stringToSign = buildStringToSign(requestURL, httpMethod, headers); + String signature = computeHmac256(stringToSign); + return "Session " + sessionToken + ":" + signature; + } + + public String getSessionToken() { + return sessionToken; + } + + public String getSessionKey() { + return sessionKey; + } + + public OffsetDateTime getExpiration() { + return expiration; + } + + public Boolean isExpired() { + return OffsetDateTime.now().isAfter(expiration); + } + + // ---- String-to-sign logic (Shared Key protocol) ---- + // Ported from StorageSharedKeyCredential.buildStringToSign(). The signing format is identical; + // the only difference is that account name is extracted from the URL rather than a constructor parameter. + + private String buildStringToSign(URL requestURL, String httpMethod, HttpHeaders headers) { + String contentLength = headers.getValue(HttpHeaderName.CONTENT_LENGTH); + contentLength = "0".equals(contentLength) ? "" : contentLength; + + String dateHeader + = (headers.getValue(X_MS_DATE) != null) ? "" : getStandardHeaderValue(headers, HttpHeaderName.DATE); + + Collator collator = Collator.getInstance(Locale.ROOT); + return String.join("\n", httpMethod, getStandardHeaderValue(headers, HttpHeaderName.CONTENT_ENCODING), + getStandardHeaderValue(headers, HttpHeaderName.CONTENT_LANGUAGE), contentLength, + getStandardHeaderValue(headers, HttpHeaderName.CONTENT_MD5), + getStandardHeaderValue(headers, HttpHeaderName.CONTENT_TYPE), dateHeader, + getStandardHeaderValue(headers, HttpHeaderName.IF_MODIFIED_SINCE), + getStandardHeaderValue(headers, HttpHeaderName.IF_MATCH), + getStandardHeaderValue(headers, HttpHeaderName.IF_NONE_MATCH), + getStandardHeaderValue(headers, HttpHeaderName.IF_UNMODIFIED_SINCE), + getStandardHeaderValue(headers, HttpHeaderName.RANGE), getAdditionalXmsHeaders(headers, collator), + getCanonicalizedResource(requestURL, collator)); + } + + private static String getStandardHeaderValue(HttpHeaders headers, HttpHeaderName headerName) { + final Header header = headers.get(headerName); + return header == null ? "" : header.getValue(); + } + + private static String getAdditionalXmsHeaders(HttpHeaders headers, Collator collator) { + List
xmsHeaders = new ArrayList<>(); + + int stringBuilderSize = 0; + for (HttpHeader header : headers) { + String headerName = header.getName(); + if (!"x-ms-".regionMatches(true, 0, headerName, 0, 5)) { + continue; + } + + String headerValue = header.getValue(); + stringBuilderSize += headerName.length() + headerValue.length(); + + xmsHeaders.add(header); + } + + if (xmsHeaders.isEmpty()) { + return ""; + } + + final StringBuilder canonicalizedHeaders = new StringBuilder(stringBuilderSize + (2 * xmsHeaders.size()) - 1); + + xmsHeaders.sort((o1, o2) -> collator.compare(o1.getName(), o2.getName())); + + for (Header xmsHeader : xmsHeaders) { + if (canonicalizedHeaders.length() > 0) { + canonicalizedHeaders.append('\n'); + } + canonicalizedHeaders.append(xmsHeader.getName().toLowerCase(Locale.ROOT)) + .append(':') + .append(xmsHeader.getValue()); + } + + return canonicalizedHeaders.toString(); + } + + private static String getCanonicalizedResource(URL requestURL, Collator collator) { + // Extract account name from hostname: "myaccount.blob.core.windows.net" -> "myaccount" + String host = requestURL.getHost(); + String accountName = host.contains(".") ? host.substring(0, host.indexOf('.')) : host; + + String absolutePath = requestURL.getPath(); + if (CoreUtils.isNullOrEmpty(absolutePath)) { + absolutePath = "/"; + } + + String query = requestURL.getQuery(); + if (CoreUtils.isNullOrEmpty(query)) { + return "/" + accountName + absolutePath; + } + + int stringBuilderSize = 1 + accountName.length() + absolutePath.length() + query.length(); + + TreeMap> pieces = new TreeMap<>(collator); + + StorageImplUtils.parseQueryParameters(query).forEachRemaining(kvp -> { + String key = urlDecode(kvp.getKey()).toLowerCase(Locale.ROOT); + + pieces.compute(key, (k, values) -> { + if (values == null) { + values = new ArrayList<>(); + } + + for (String value : kvp.getValue().split(",")) { + values.add(urlDecode(value)); + } + + return values; + }); + }); + + stringBuilderSize += pieces.size(); + + StringBuilder canonicalizedResource + = new StringBuilder(stringBuilderSize).append('/').append(accountName).append(absolutePath); + + for (Map.Entry> queryParam : pieces.entrySet()) { + List queryParamValues = queryParam.getValue(); + queryParamValues.sort(collator); + canonicalizedResource.append('\n').append(queryParam.getKey()).append(':'); + + int size = queryParamValues.size(); + for (int i = 0; i < size; i++) { + String queryParamValue = queryParamValues.get(i); + if (i > 0) { + canonicalizedResource.append(','); + } + + canonicalizedResource.append(queryParamValue); + } + } + + return canonicalizedResource.toString(); + } +} diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobTestBase.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobTestBase.java index 99d925bff60c..5406fd69e941 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobTestBase.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/BlobTestBase.java @@ -196,7 +196,11 @@ public void beforeTest() { TestProxySanitizerType.HEADER), new TestProxySanitizer("x-ms-rename-source", "((?<=http://|https://)([^/?]+)|sig=(.*))", "REDACTED", TestProxySanitizerType.HEADER), - new TestProxySanitizer("skoid=([^&]+)", "REDACTED", TestProxySanitizerType.URL))); + new TestProxySanitizer("skoid=([^&]+)", "REDACTED", TestProxySanitizerType.URL), + new TestProxySanitizer("(?.*?)", "REDACTED", + TestProxySanitizerType.BODY_REGEX).setGroupForReplace("secret"), + new TestProxySanitizer("(?.*?)", "REDACTED", + TestProxySanitizerType.BODY_REGEX).setGroupForReplace("secret"))); } // Ignore changes to the order of query parameters and wholly ignore the 'sv' (service version) query parameter diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java index f46116acdbb5..4604b89297a8 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerApiTests.java @@ -10,6 +10,8 @@ import com.azure.core.test.utils.MockTokenCredential; import com.azure.core.util.Context; import com.azure.identity.DefaultAzureCredentialBuilder; +import com.azure.storage.blob.implementation.models.AuthenticationType; +import com.azure.storage.blob.implementation.models.CreateSessionResponse; import com.azure.storage.blob.models.AccessTier; import com.azure.storage.blob.models.AppendBlobItem; import com.azure.storage.blob.models.BlobAccessPolicy; @@ -2128,4 +2130,20 @@ public void getBlobContainerUrlEncodesContainerName() { // then: // assertThrows(BlobStorageException.class, () -> // } + + @Test + // @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void createSessionReturnsTokenAndKey() { + BlobContainerClient oauthCc = getOAuthServiceClient().getBlobContainerClient(cc.getBlobContainerName()); + Response response = oauthCc.createSessionWithResponse(null, null); + + assertResponseStatusCode(response, 201); + CreateSessionResponse session = response.getValue(); + assertNotNull(session.getId()); + assertNotNull(session.getExpiration()); + assertNotNull(session.getCredentials()); + assertNotNull(session.getCredentials().getSessionToken()); + assertNotNull(session.getCredentials().getSessionKey()); + assertEquals(AuthenticationType.HMAC, session.getAuthenticationType()); + } } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java index 04ebc06dc2b6..eca3a1be27a9 100644 --- a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/ContainerAsyncApiTests.java @@ -12,6 +12,8 @@ import com.azure.core.util.Context; import com.azure.core.util.polling.PollerFlux; import com.azure.identity.DefaultAzureCredentialBuilder; +import com.azure.storage.blob.implementation.models.AuthenticationType; +import com.azure.storage.blob.implementation.models.CreateSessionResponse; import com.azure.storage.blob.models.*; import com.azure.storage.blob.options.BlobContainerCreateOptions; import com.azure.storage.blob.options.BlobParallelUploadOptions; @@ -2142,4 +2144,22 @@ public void getBlobContainerUrlEncodesContainerName() { assertTrue(containerClient.getBlobContainerUrl().contains("my%20container")); } + + @Test + // @RequiredServiceVersion(clazz = BlobServiceVersion.class, min = "2026-06-06") + public void createSessionReturnsTokenAndKey() { + BlobContainerAsyncClient oauthCcAsync + = getOAuthServiceAsyncClient().getBlobContainerAsyncClient(ccAsync.getBlobContainerName()); + + StepVerifier.create(oauthCcAsync.createSessionWithResponse()).assertNext(response -> { + assertEquals(201, response.getStatusCode()); + CreateSessionResponse session = response.getValue(); + assertNotNull(session.getId()); + assertNotNull(session.getExpiration()); + assertNotNull(session.getCredentials()); + assertNotNull(session.getCredentials().getSessionToken()); + assertNotNull(session.getCredentials().getSessionKey()); + assertEquals(AuthenticationType.HMAC, session.getAuthenticationType()); + }).verifyComplete(); + } } diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/SessionTestHelper.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/SessionTestHelper.java new file mode 100644 index 000000000000..7196ebce8f1f --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/SessionTestHelper.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob.implementation.util; + +import java.time.OffsetDateTime; + +/** + * Shared test constants and factories for session-based auth tests. + */ +final class SessionTestHelper { + + // A valid Base64-encoded 32-byte key for testing + static final String TEST_SESSION_KEY = "dGVzdFNlc3Npb25LZXkxMjM0NTY3ODkwMTIzNDU2Nzg5MA=="; + static final String TEST_SESSION_TOKEN = "test-session-token-abc123"; + static final String TEST_CONTAINER_NAME = "testcontainer"; + + static StorageSessionCredential createCredential(OffsetDateTime expiration) { + return new StorageSessionCredential(TEST_SESSION_TOKEN, TEST_SESSION_KEY, expiration); + } + + static StorageSessionCredential createValidCredential() { + return createCredential(OffsetDateTime.now().plusHours(1)); + } + + static StorageSessionCredential createExpiredCredential() { + return createCredential(OffsetDateTime.now().minusMinutes(5)); + } + + private SessionTestHelper() { + } +} diff --git a/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/StorageSessionCredentialTest.java b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/StorageSessionCredentialTest.java new file mode 100644 index 000000000000..3b8dd46e4731 --- /dev/null +++ b/sdk/storage/azure-storage-blob/src/test/java/com/azure/storage/blob/implementation/util/StorageSessionCredentialTest.java @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob.implementation.util; + +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpHeaders; +import com.azure.storage.common.implementation.StorageImplUtils; +import org.junit.jupiter.api.Test; + +import java.net.MalformedURLException; +import java.net.URL; +import java.time.OffsetDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class StorageSessionCredentialTest { + + @Test + public void signRequestWithSessionKey() { + // Given a known session key and a known string-to-sign + StorageSessionCredential credential = SessionTestHelper.createValidCredential(); + + String stringToSign = "GET\n\n\n\n\n\n\n\n\n\n\n\n" + "x-ms-date:Mon, 31 Mar 2025 00:00:00 GMT\n" + + "x-ms-version:2025-01-05\n" + "/myaccount/mycontainer/myblob"; + + // When computing HMAC + String signature = credential.computeHmac256(stringToSign); + + // Then it matches the expected HMAC from StorageImplUtils + String expected = StorageImplUtils.computeHMac256(SessionTestHelper.TEST_SESSION_KEY, stringToSign); + assertEquals(expected, signature); + } + + @Test + public void generateAuthorizationHeaderFormat() throws MalformedURLException { + // Given a session credential + StorageSessionCredential credential = SessionTestHelper.createValidCredential(); + + // And a request URL and headers + URL url = new URL("https://myaccount.blob.core.windows.net/mycontainer/myblob"); + HttpHeaders headers + = new HttpHeaders().set(HttpHeaderName.fromString("x-ms-date"), "Mon, 31 Mar 2025 00:00:00 GMT") + .set(HttpHeaderName.fromString("x-ms-version"), "2025-01-05") + .set(HttpHeaderName.CONTENT_LENGTH, "0"); + + // When generating the authorization header + String authHeader = credential.generateAuthorizationHeader(url, "GET", headers); + + // Then it uses the "Session" scheme with the token and a signature + assertTrue(authHeader.startsWith("Session " + SessionTestHelper.TEST_SESSION_TOKEN + ":"), + "Authorization header should start with 'Session :' but was: " + authHeader); + + // And the signature portion is a valid Base64 HMAC + String signaturePart = authHeader.substring(authHeader.indexOf(':') + 1); + assertTrue(signaturePart.length() > 0, "Signature should not be empty"); + } + + @Test + public void isExpiredReturnsTrueWhenPastExpiration() { + StorageSessionCredential credential = SessionTestHelper.createExpiredCredential(); + + assertTrue(credential.isExpired(), "Credential should be expired when expiration is in the past"); + } + + @Test + public void isExpiredReturnsFalseWhenBeforeExpiration() { + StorageSessionCredential credential = SessionTestHelper.createValidCredential(); + + assertFalse(credential.isExpired(), "Credential should not be expired when expiration is in the future"); + } +} diff --git a/sdk/storage/azure-storage-blob/swagger/README.md b/sdk/storage/azure-storage-blob/swagger/README.md index 292d2f7c231d..e3f3f4ead37b 100644 --- a/sdk/storage/azure-storage-blob/swagger/README.md +++ b/sdk/storage/azure-storage-blob/swagger/README.md @@ -16,7 +16,7 @@ autorest ### Code generation settings ``` yaml use: '@autorest/java@4.1.63' -input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/15d7f54a5389d5906ffb4e56bb2f38fe5525c0d3/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-06-06/blob.json +input-file: https://raw.githubusercontent.com/nickliu-msft/azure-rest-api-specs/013866b01623e6f2cc6c313b44c9c6460de3e91e/specification/storage/data-plane/Microsoft.BlobStorage/stable/2026-10-06/blob.json java: true output-folder: ../ namespace: com.azure.storage.blob diff --git a/sdk/storage/azure-storage-common/ci.system.properties b/sdk/storage/azure-storage-common/ci.system.properties index 1d1c46cd13b4..907af8fd671d 100644 --- a/sdk/storage/azure-storage-common/ci.system.properties +++ b/sdk/storage/azure-storage-common/ci.system.properties @@ -1,2 +1,2 @@ -AZURE_LIVE_TEST_SERVICE_VERSION=V2026_06_06 +AZURE_LIVE_TEST_SERVICE_VERSION=V2026_04_06 AZURE_STORAGE_SAS_SERVICE_VERSION=2026-06-06