diff --git a/Microsoft.Azure.Cosmos/src/Fluent/Settings/ClientEncryptionPolicyDefinition.cs b/Microsoft.Azure.Cosmos/src/Fluent/Settings/ClientEncryptionPolicyDefinition.cs index 7b103f74b5..024f2b05ea 100644 --- a/Microsoft.Azure.Cosmos/src/Fluent/Settings/ClientEncryptionPolicyDefinition.cs +++ b/Microsoft.Azure.Cosmos/src/Fluent/Settings/ClientEncryptionPolicyDefinition.cs @@ -9,19 +9,55 @@ namespace Microsoft.Azure.Cosmos.Fluent /// /// fluent definition. + /// The should be initialized with + /// policyFormatVersion 2 and "Deterministic" encryption type, if "id" property or properties which are part of partition key need to be encrypted. + /// All partition key property values included as part of have to be JSON strings. /// + /// + /// This example shows how to create a using . + /// + /// + /// + /// public sealed class ClientEncryptionPolicyDefinition { private readonly Collection clientEncryptionIncludedPaths = new Collection(); private readonly ContainerBuilder parent; private readonly Action attachCallback; + private readonly int policyFormatVersion; internal ClientEncryptionPolicyDefinition( ContainerBuilder parent, - Action attachCallback) + Action attachCallback, + int policyFormatVersion = 1) { this.parent = parent; this.attachCallback = attachCallback; + this.policyFormatVersion = (policyFormatVersion > 2 || policyFormatVersion < 1) ? throw new ArgumentException($"Supported versions of client encryption policy are 1 and 2. ") : policyFormatVersion; } /// @@ -41,7 +77,7 @@ public ClientEncryptionPolicyDefinition WithIncludedPath(ClientEncryptionInclude /// An instance of the parent. public ContainerBuilder Attach() { - this.attachCallback(new ClientEncryptionPolicy(this.clientEncryptionIncludedPaths)); + this.attachCallback(new ClientEncryptionPolicy(this.clientEncryptionIncludedPaths, this.policyFormatVersion)); return this.parent; } } diff --git a/Microsoft.Azure.Cosmos/src/Fluent/Settings/ContainerBuilder.cs b/Microsoft.Azure.Cosmos/src/Fluent/Settings/ContainerBuilder.cs index 53b2e1205d..567396646f 100644 --- a/Microsoft.Azure.Cosmos/src/Fluent/Settings/ContainerBuilder.cs +++ b/Microsoft.Azure.Cosmos/src/Fluent/Settings/ContainerBuilder.cs @@ -90,12 +90,14 @@ ChangeFeedPolicyDefinition WithChangeFeedPolicy(TimeSpan retention) /// /// Defines the ClientEncryptionPolicy for Azure Cosmos container /// + /// Version of the client encryption policy definition. Current supported versions are 1 and 2. Default version is 1. /// An instance of . - public ClientEncryptionPolicyDefinition WithClientEncryptionPolicy() + public ClientEncryptionPolicyDefinition WithClientEncryptionPolicy(int policyFormatVersion = 1) { return new ClientEncryptionPolicyDefinition( this, - (clientEncryptionPolicy) => this.AddClientEncryptionPolicy(clientEncryptionPolicy)); + (clientEncryptionPolicy) => this.AddClientEncryptionPolicy(clientEncryptionPolicy), + policyFormatVersion); } /// diff --git a/Microsoft.Azure.Cosmos/src/Resource/Settings/ClientEncryptionPolicy.cs b/Microsoft.Azure.Cosmos/src/Resource/Settings/ClientEncryptionPolicy.cs index ab3dcc5e74..6be228d73b 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Settings/ClientEncryptionPolicy.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Settings/ClientEncryptionPolicy.cs @@ -12,19 +12,53 @@ namespace Microsoft.Azure.Cosmos using Newtonsoft.Json.Linq; /// - /// Client encryption policy. + /// The should be initialized with + /// policyFormatVersion 2 and "Deterministic" encryption type, if "id" property or properties which are part of partition key need to be encrypted. + /// All partition key property values have to be JSON strings. /// + /// + /// This example shows how to create a . + /// + /// paths = new Collection() + /// { + /// new ClientEncryptionIncludedPath() + /// { + /// Path = partitionKeyPath, + /// ClientEncryptionKeyId = "key1", + /// EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + /// EncryptionType = "Deterministic" + /// }, + /// new ClientEncryptionIncludedPath() + /// { + /// Path = "/id", + /// ClientEncryptionKeyId = "key2", + /// EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + /// EncryptionType = "Deterministic" + /// }, + /// }; + /// + /// ContainerProperties setting = new ContainerProperties() + /// { + /// Id = containerName, + /// PartitionKeyPath = partitionKeyPath, + /// ClientEncryptionPolicy = new ClientEncryptionPolicy(includedPaths:paths, policyFormatVersion:2) + /// }; + /// ]]> + /// + /// public sealed class ClientEncryptionPolicy { /// /// Initializes a new instance of the class. /// /// List of paths to include in the policy definition. - public ClientEncryptionPolicy(IEnumerable includedPaths) + /// Version of the client encryption policy definition. Current supported versions are 1 and 2. Default version is 1. + public ClientEncryptionPolicy(IEnumerable includedPaths, int policyFormatVersion = 1) { - ClientEncryptionPolicy.ValidateIncludedPaths(includedPaths); + this.PolicyFormatVersion = (policyFormatVersion > 2 || policyFormatVersion < 1) ? throw new ArgumentException($"Supported versions of client encryption policy are 1 and 2. ") : policyFormatVersion; + ClientEncryptionPolicy.ValidateIncludedPaths(includedPaths, policyFormatVersion); this.IncludedPaths = includedPaths; - this.PolicyFormatVersion = 1; } [JsonConstructor] @@ -33,7 +67,7 @@ private ClientEncryptionPolicy() } /// - /// Paths of the item that need encryption along with path-specific settings. + /// Paths of the item that need encryption along with path-specific settings. /// [JsonProperty(PropertyName = "includedPaths")] public IEnumerable IncludedPaths @@ -55,43 +89,60 @@ public IEnumerable IncludedPaths internal IDictionary AdditionalProperties { get; private set; } /// - /// Ensures that partition key paths are not specified in the client encryption policy for encryption. + /// Ensures that partition key paths specified in the client encryption policy for encryption are encrypted using Deterministic encryption algorithm. /// /// Tokens corresponding to validated partition key. - internal void ValidatePartitionKeyPathsAreNotEncrypted(IReadOnlyList> partitionKeyPathTokens) + internal void ValidatePartitionKeyPathsIfEncrypted(IReadOnlyList> partitionKeyPathTokens) { Debug.Assert(partitionKeyPathTokens != null); - IEnumerable propertiesToEncrypt = this.IncludedPaths.Select(p => p.Path.Substring(1)); + foreach (IReadOnlyList tokensInPath in partitionKeyPathTokens) { Debug.Assert(tokensInPath != null); if (tokensInPath.Count > 0) { string topLevelToken = tokensInPath.First(); - if (propertiesToEncrypt.Contains(topLevelToken)) + + // paths in included paths start with "/". Get the ClientEncryptionIncludedPath and validate. + IEnumerable encryptedPartitionKeyPath = this.IncludedPaths.Where(p => p.Path.Substring(1).Equals(topLevelToken)); + + if (encryptedPartitionKeyPath.Any()) { - throw new ArgumentException($"Paths which are part of the partition key may not be included in the {nameof(ClientEncryptionPolicy)}.", nameof(ContainerProperties.ClientEncryptionPolicy)); + if (this.PolicyFormatVersion < 2) + { + throw new ArgumentException($"Path: /{topLevelToken} which is part of the partition key cannot be encrypted with PolicyFormatVersion: {this.PolicyFormatVersion}. Please use PolicyFormatVersion: 2. "); + } + + // for the ClientEncryptionIncludedPath found check the encryption type. + if (encryptedPartitionKeyPath.Select(et => et.EncryptionType).FirstOrDefault() != "Deterministic") + { + throw new ArgumentException($"Path: /{topLevelToken} which is part of the partition key has to be encrypted with Deterministic type Encryption."); + } } } } } - private static void ValidateIncludedPaths(IEnumerable clientEncryptionIncludedPath) + private static void ValidateIncludedPaths( + IEnumerable clientEncryptionIncludedPath, + int policyFormatVersion) { List includedPathsList = new List(); foreach (ClientEncryptionIncludedPath path in clientEncryptionIncludedPath) { - ClientEncryptionPolicy.ValidateClientEncryptionIncludedPath(path); + ClientEncryptionPolicy.ValidateClientEncryptionIncludedPath(path, policyFormatVersion); if (includedPathsList.Contains(path.Path)) { - throw new ArgumentException("Duplicate Path found.", nameof(clientEncryptionIncludedPath)); + throw new ArgumentException($"Duplicate Path found: {path.Path}."); } includedPathsList.Add(path.Path); } } - private static void ValidateClientEncryptionIncludedPath(ClientEncryptionIncludedPath clientEncryptionIncludedPath) + private static void ValidateClientEncryptionIncludedPath( + ClientEncryptionIncludedPath clientEncryptionIncludedPath, + int policyFormatVersion) { if (clientEncryptionIncludedPath == null) { @@ -104,8 +155,7 @@ private static void ValidateClientEncryptionIncludedPath(ClientEncryptionInclude } if (clientEncryptionIncludedPath.Path[0] != '/' - || clientEncryptionIncludedPath.Path.LastIndexOf('/') != 0 - || string.Equals(clientEncryptionIncludedPath.Path.Substring(1), "id")) + || clientEncryptionIncludedPath.Path.LastIndexOf('/') != 0) { throw new ArgumentException($"Invalid path '{clientEncryptionIncludedPath.Path ?? string.Empty}'."); } @@ -120,6 +170,19 @@ private static void ValidateClientEncryptionIncludedPath(ClientEncryptionInclude throw new ArgumentNullException(nameof(clientEncryptionIncludedPath.EncryptionType)); } + if (string.Equals(clientEncryptionIncludedPath.Path.Substring(1), "id")) + { + if (policyFormatVersion < 2) + { + throw new ArgumentException($"Path: {clientEncryptionIncludedPath.Path} cannot be encrypted with PolicyFormatVersion: {policyFormatVersion}. Please use PolicyFormatVersion: 2. "); + } + + if (clientEncryptionIncludedPath.EncryptionType != "Deterministic") + { + throw new ArgumentException($"Only Deterministic encryption type is supported for path: {clientEncryptionIncludedPath.Path}. "); + } + } + if (!string.Equals(clientEncryptionIncludedPath.EncryptionType, "Deterministic") && !string.Equals(clientEncryptionIncludedPath.EncryptionType, "Randomized")) { diff --git a/Microsoft.Azure.Cosmos/src/Resource/Settings/ContainerProperties.cs b/Microsoft.Azure.Cosmos/src/Resource/Settings/ContainerProperties.cs index 80280573c2..fb1eb0a165 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Settings/ContainerProperties.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Settings/ContainerProperties.cs @@ -703,7 +703,7 @@ internal void ValidateRequiredProperties() if (this.ClientEncryptionPolicy != null) { - this.ClientEncryptionPolicy.ValidatePartitionKeyPathsAreNotEncrypted(this.PartitionKeyPathTokens); + this.ClientEncryptionPolicy.ValidatePartitionKeyPathsIfEncrypted(this.PartitionKeyPathTokens); } } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosContainerTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosContainerTests.cs index 71e8ee5970..a17de1087c 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosContainerTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosContainerTests.cs @@ -1365,10 +1365,17 @@ public async Task ClientEncryptionPolicyTest() { new ClientEncryptionIncludedPath() { - Path = "/path1", + Path = partitionKeyPath, ClientEncryptionKeyId = "dekId1", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", - EncryptionType = "Randomized" + EncryptionType = "Deterministic" + }, + new ClientEncryptionIncludedPath() + { + Path = "/id", + ClientEncryptionKeyId = "dekId2", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + EncryptionType = "Deterministic" }, new ClientEncryptionIncludedPath() { @@ -1383,7 +1390,7 @@ public async Task ClientEncryptionPolicyTest() { Id = containerName, PartitionKey = new Documents.PartitionKeyDefinition() { Paths = new Collection { partitionKeyPath }, Kind = Documents.PartitionKind.Hash }, - ClientEncryptionPolicy = new ClientEncryptionPolicy(paths) + ClientEncryptionPolicy = new ClientEncryptionPolicy(includedPaths:paths,policyFormatVersion:2) }; ContainerResponse containerResponse = await this.cosmosDatabase.CreateContainerIfNotExistsAsync(setting); @@ -1391,8 +1398,64 @@ public async Task ClientEncryptionPolicyTest() Container container = containerResponse; ContainerProperties responseSettings = containerResponse; - Assert.AreEqual(2, responseSettings.ClientEncryptionPolicy.IncludedPaths.Count()); + Assert.AreEqual(3, responseSettings.ClientEncryptionPolicy.IncludedPaths.Count()); ClientEncryptionIncludedPath includedPath = responseSettings.ClientEncryptionPolicy.IncludedPaths.ElementAt(0); + Assert.AreEqual(partitionKeyPath, includedPath.Path); + Assert.AreEqual("dekId1", includedPath.ClientEncryptionKeyId); + Assert.AreEqual("AEAD_AES_256_CBC_HMAC_SHA256", includedPath.EncryptionAlgorithm); + Assert.AreEqual("Deterministic", includedPath.EncryptionType); + + includedPath = responseSettings.ClientEncryptionPolicy.IncludedPaths.ElementAt(1); + Assert.AreEqual("/id", includedPath.Path); + Assert.AreEqual("dekId2", includedPath.ClientEncryptionKeyId); + Assert.AreEqual("AEAD_AES_256_CBC_HMAC_SHA256", includedPath.EncryptionAlgorithm); + Assert.AreEqual("Deterministic", includedPath.EncryptionType); + + includedPath = responseSettings.ClientEncryptionPolicy.IncludedPaths.ElementAt(2); + Assert.AreEqual("/path2", includedPath.Path); + Assert.AreEqual("dekId2", includedPath.ClientEncryptionKeyId); + Assert.AreEqual("AEAD_AES_256_CBC_HMAC_SHA256", includedPath.EncryptionAlgorithm); + Assert.AreEqual("Deterministic", includedPath.EncryptionType); + + ContainerResponse readResponse = await container.ReadContainerAsync(); + Assert.AreEqual(HttpStatusCode.Created, containerResponse.StatusCode); + Assert.IsNotNull(readResponse.Resource.ClientEncryptionPolicy); + + // version 1 test. + containerName = Guid.NewGuid().ToString(); + partitionKeyPath = "/users"; + paths = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = "/path1", + ClientEncryptionKeyId = "dekId1", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + EncryptionType = "Randomized" + }, + new ClientEncryptionIncludedPath() + { + Path = "/path2", + ClientEncryptionKeyId = "dekId2", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + EncryptionType = "Deterministic" + } + }; + + setting = new ContainerProperties() + { + Id = containerName, + PartitionKey = new Documents.PartitionKeyDefinition() { Paths = new Collection { partitionKeyPath }, Kind = Documents.PartitionKind.Hash }, + ClientEncryptionPolicy = new ClientEncryptionPolicy(paths) + }; + + containerResponse = await this.cosmosDatabase.CreateContainerIfNotExistsAsync(setting); + Assert.AreEqual(HttpStatusCode.Created, containerResponse.StatusCode); + container = containerResponse; + responseSettings = containerResponse; + + Assert.AreEqual(2, responseSettings.ClientEncryptionPolicy.IncludedPaths.Count()); + includedPath = responseSettings.ClientEncryptionPolicy.IncludedPaths.ElementAt(0); Assert.AreEqual("/path1", includedPath.Path); Assert.AreEqual("dekId1", includedPath.ClientEncryptionKeyId); Assert.AreEqual("AEAD_AES_256_CBC_HMAC_SHA256", includedPath.EncryptionAlgorithm); @@ -1404,9 +1467,9 @@ public async Task ClientEncryptionPolicyTest() Assert.AreEqual("AEAD_AES_256_CBC_HMAC_SHA256", includedPath.EncryptionAlgorithm); Assert.AreEqual("Deterministic", includedPath.EncryptionType); - ContainerResponse readResponse = await container.ReadContainerAsync(); + readResponse = await container.ReadContainerAsync(); Assert.AreEqual(HttpStatusCode.Created, containerResponse.StatusCode); - Assert.IsNotNull(readResponse.Resource.ClientEncryptionPolicy); + Assert.IsNotNull(readResponse.Resource.ClientEncryptionPolicy); // replace without updating CEP should be successful readResponse.Resource.IndexingPolicy = new Cosmos.IndexingPolicy() @@ -1464,7 +1527,7 @@ public async Task ClientEncryptionPolicyFailureTest() } catch (ArgumentException ex) { - Assert.IsTrue(ex.Message.Contains("EncryptionAlgorithm should be 'AEAD_AES_256_CBC_HMAC_SHA256'.")); + Assert.IsTrue(ex.Message.Contains("EncryptionAlgorithm should be 'AEAD_AES_256_CBC_HMAC_SHA256'."), ex.Message); } try @@ -1478,15 +1541,16 @@ public async Task ClientEncryptionPolicyFailureTest() }; Collection pathsList = new Collection() - { - new ClientEncryptionIncludedPath() - { - Path = "/path1", - ClientEncryptionKeyId = "dekId1", - EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", - EncryptionType = "Randomized" - }, - }; + { + new ClientEncryptionIncludedPath() + { + Path = "/path1", + ClientEncryptionKeyId = "dekId1", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + EncryptionType = "Randomized" + }, + }; + pathsList.Add(path1); ContainerProperties setting = new ContainerProperties() @@ -1500,7 +1564,7 @@ public async Task ClientEncryptionPolicyFailureTest() } catch (ArgumentException ex) { - Assert.IsTrue(ex.Message.Contains("Duplicate Path found.")); + Assert.IsTrue(ex.Message.Contains("Duplicate Path found: /path1."), ex.Message); } try @@ -1514,6 +1578,105 @@ public async Task ClientEncryptionPolicyFailureTest() EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", EncryptionType = "Randomized" }, + new ClientEncryptionIncludedPath() + { + Path = "/path1", + ClientEncryptionKeyId = "dekId1", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + EncryptionType = "Deterministic" + }, + }; + + ContainerProperties setting = new ContainerProperties() + { + Id = containerName, + PartitionKey = new Documents.PartitionKeyDefinition() { Paths = new Collection { partitionKeyPath }, Kind = Documents.PartitionKind.Hash }, + ClientEncryptionPolicy = new ClientEncryptionPolicy(pathsToEncryptWithPartitionKey, 2) + }; + + await this.cosmosDatabase.CreateContainerAsync(setting); + Assert.Fail("Creating container should have failed."); + } + catch (ArgumentException ex) + { + Assert.IsTrue(ex.Message.Contains("Path: /users which is part of the partition key has to be encrypted with Deterministic type Encryption."), ex.Message); + } + + try + { + Collection pathsToEncryptWithPartitionKey = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = partitionKeyPath, + ClientEncryptionKeyId = "dekId1", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + EncryptionType = "Deterministic" + }, + new ClientEncryptionIncludedPath() + { + Path = "/id", + ClientEncryptionKeyId = "dekId1", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + EncryptionType = "Randomized" + }, + }; + + ContainerProperties setting = new ContainerProperties() + { + Id = containerName, + PartitionKey = new Documents.PartitionKeyDefinition() { Paths = new Collection { partitionKeyPath }, Kind = Documents.PartitionKind.Hash }, + ClientEncryptionPolicy = new ClientEncryptionPolicy(pathsToEncryptWithPartitionKey, 2) + }; + + await this.cosmosDatabase.CreateContainerAsync(setting); + Assert.Fail("Creating container should have failed."); + } + catch (ArgumentException ex) + { + Assert.IsTrue(ex.Message.Contains("Only Deterministic encryption type is supported for path: /id."), ex.Message); + } + + // failure due to policy format version 1. for Pk and Id + try + { + Collection pathsToEncryptWithPartitionKey = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = partitionKeyPath, + ClientEncryptionKeyId = "dekId1", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + EncryptionType = "Deterministic" + }, + }; + + ContainerProperties setting = new ContainerProperties() + { + Id = containerName, + PartitionKey = new Documents.PartitionKeyDefinition() { Paths = new Collection { partitionKeyPath }, Kind = Documents.PartitionKind.Hash }, + ClientEncryptionPolicy = new ClientEncryptionPolicy(pathsToEncryptWithPartitionKey) + }; + + await this.cosmosDatabase.CreateContainerAsync(setting); + Assert.Fail("Creating container should have failed."); + } + catch (ArgumentException ex) + { + Assert.IsTrue(ex.Message.Contains("Path: /users which is part of the partition key cannot be encrypted with PolicyFormatVersion: 1. Please use PolicyFormatVersion: 2."), ex.Message); + } + + try + { + Collection pathsToEncryptWithPartitionKey = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = "/id", + ClientEncryptionKeyId = "dekId1", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + EncryptionType = "Deterministic" + }, }; ContainerProperties setting = new ContainerProperties() @@ -1528,7 +1691,65 @@ public async Task ClientEncryptionPolicyFailureTest() } catch (ArgumentException ex) { - Assert.IsTrue(ex.Message.Contains("Paths which are part of the partition key may not be included in the ClientEncryptionPolicy.")); + Assert.IsTrue(ex.Message.Contains("Path: /id cannot be encrypted with PolicyFormatVersion: 1. Please use PolicyFormatVersion: 2."), ex.Message); + } + + // hierarchical partition keys + try + { + Collection pathsToEncryptWithPartitionKey = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = "/id", + ClientEncryptionKeyId = "dekId1", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + EncryptionType = "Randomized" + }, + }; + + ContainerProperties setting = new ContainerProperties() + { + Id = containerName, + PartitionKeyPaths = new Collection { "/path1", "/id" }, + ClientEncryptionPolicy = new ClientEncryptionPolicy(pathsToEncryptWithPartitionKey, 2) + }; + + await this.cosmosDatabase.CreateContainerAsync(setting); + Assert.Fail("Creating container should have failed."); + } + catch (ArgumentException ex) + { + Assert.IsTrue(ex.Message.Contains("Only Deterministic encryption type is supported for path: /id."), ex.Message); + } + + // hierarchical partition keys + try + { + Collection pathsToEncryptWithPartitionKey = new Collection() + { + new ClientEncryptionIncludedPath() + { + Path = partitionKeyPath, + ClientEncryptionKeyId = "dekId1", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + EncryptionType = "Randomized" + }, + }; + + ContainerProperties setting = new ContainerProperties() + { + Id = containerName, + PartitionKeyPaths = new Collection { partitionKeyPath, "/path1" }, + ClientEncryptionPolicy = new ClientEncryptionPolicy(pathsToEncryptWithPartitionKey, 2) + }; + + await this.cosmosDatabase.CreateContainerAsync(setting); + Assert.Fail("Creating container should have failed."); + } + catch (ArgumentException ex) + { + Assert.IsTrue(ex.Message.Contains("Path: /users which is part of the partition key has to be encrypted with Deterministic type Encryption."), ex.Message); } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Fluent/ContainerSettingsTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Fluent/ContainerSettingsTests.cs index d038032f93..ca8aaadec1 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Fluent/ContainerSettingsTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Fluent/ContainerSettingsTests.cs @@ -137,7 +137,7 @@ public async Task ContainerContractTest() Assert.AreEqual(4, spatialPath.SpatialTypes.Count); // All SpatialTypes are returned Assert.AreEqual(1, responseProperties.ClientEncryptionPolicy.IncludedPaths.Count()); - Assert.IsTrue(responseProperties.ClientEncryptionPolicy.PolicyFormatVersion <= 1); + Assert.IsTrue(responseProperties.ClientEncryptionPolicy.PolicyFormatVersion <= 2); ClientEncryptionIncludedPath clientEncryptionIncludedPath = responseProperties.ClientEncryptionPolicy.IncludedPaths.First(); Assert.IsTrue(this.VerifyClientEncryptionIncludedPath(clientEncryptionIncludedPath1, clientEncryptionIncludedPath)); } @@ -601,26 +601,27 @@ public async Task WithClientEncryptionPolicyTest() await TestCommon.CreateClientEncryptionKey("dekId1", databaseInlineCore); await TestCommon.CreateClientEncryptionKey("dekId2", databaseInlineCore); + // version 2 string containerName = Guid.NewGuid().ToString(); string partitionKeyPath = "/users"; ClientEncryptionIncludedPath path1 = new ClientEncryptionIncludedPath() { - Path = "/path1", + Path = partitionKeyPath, ClientEncryptionKeyId = "dekId1", - EncryptionType = "Randomized", + EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256" }; ClientEncryptionIncludedPath path2 = new ClientEncryptionIncludedPath() { - Path = "/path2", + Path = "/id", ClientEncryptionKeyId = "dekId2", - EncryptionType = "Randomized", + EncryptionType = "Deterministic", EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", }; - + ContainerResponse containerResponse = await this.database.DefineContainer(containerName, partitionKeyPath) - .WithClientEncryptionPolicy() + .WithClientEncryptionPolicy(policyFormatVersion:2) .WithIncludedPath(path1) .WithIncludedPath(path2) .Attach() @@ -641,6 +642,47 @@ public async Task WithClientEncryptionPolicyTest() Assert.AreEqual(HttpStatusCode.Created, containerResponse.StatusCode); Assert.IsNotNull(readResponse.Resource.ClientEncryptionPolicy); + // version 1 + containerName = Guid.NewGuid().ToString(); + partitionKeyPath = "/users"; + path1 = new ClientEncryptionIncludedPath() + { + Path = "/path1", + ClientEncryptionKeyId = "dekId1", + EncryptionType = "Randomized", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256" + }; + + path2 = new ClientEncryptionIncludedPath() + { + Path = "/path2", + ClientEncryptionKeyId = "dekId2", + EncryptionType = "Randomized", + EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256", + }; + + containerResponse = await this.database.DefineContainer(containerName, partitionKeyPath) + .WithClientEncryptionPolicy() + .WithIncludedPath(path1) + .WithIncludedPath(path2) + .Attach() + .CreateAsync(); + + Assert.AreEqual(HttpStatusCode.Created, containerResponse.StatusCode); + container = containerResponse; + responseSettings = containerResponse; + + Assert.IsNotNull(responseSettings.ClientEncryptionPolicy); + Assert.AreEqual(2, responseSettings.ClientEncryptionPolicy.IncludedPaths.Count()); + clientEncryptionIncludedPath = responseSettings.ClientEncryptionPolicy.IncludedPaths.First(); + Assert.IsTrue(this.VerifyClientEncryptionIncludedPath(path1, clientEncryptionIncludedPath)); + clientEncryptionIncludedPath = responseSettings.ClientEncryptionPolicy.IncludedPaths.Last(); + Assert.IsTrue(this.VerifyClientEncryptionIncludedPath(path2, clientEncryptionIncludedPath)); + + readResponse = await container.ReadContainerAsync(); + Assert.AreEqual(HttpStatusCode.Created, containerResponse.StatusCode); + Assert.IsNotNull(readResponse.Resource.ClientEncryptionPolicy); + // update CEP and replace container readResponse.Resource.ClientEncryptionPolicy = null; try @@ -737,6 +779,76 @@ public async Task WithClientEncryptionPolicyFailureTest() { Assert.IsTrue(ex.Message.Contains("EncryptionAlgorithm should be 'AEAD_AES_256_CBC_HMAC_SHA256'. ")); } + + // invalid policy version for partition key encryption + path1.EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256"; + path1.Path = partitionKeyPath; + try + { + ContainerResponse containerResponse = await this.database.DefineContainer(containerName, partitionKeyPath) + .WithClientEncryptionPolicy() + .WithIncludedPath(path1) + .Attach() + .CreateAsync(); + + Assert.Fail("CreateCollection with invalid ClientEncryptionPolicy should have failed."); + } + catch (ArgumentException ex) + { + Assert.IsTrue(ex.Message.Contains("Path: /users which is part of the partition key cannot be encrypted with PolicyFormatVersion: 1. Please use PolicyFormatVersion: 2."), ex.Message); + } + + // invalid policy version for id encryption + path1.Path = "/id"; + try + { + ContainerResponse containerResponse = await this.database.DefineContainer(containerName, partitionKeyPath) + .WithClientEncryptionPolicy() + .WithIncludedPath(path1) + .Attach() + .CreateAsync(); + + Assert.Fail("CreateCollection with invalid ClientEncryptionPolicy should have failed."); + } + catch (ArgumentException ex) + { + Assert.IsTrue(ex.Message.Contains("Path: /id cannot be encrypted with PolicyFormatVersion: 1. Please use PolicyFormatVersion: 2."), ex.Message); + } + + // invalid encryption type for id encryption + path1.EncryptionType = "Randomized"; + path1.Path = partitionKeyPath; + try + { + ContainerResponse containerResponse = await this.database.DefineContainer(containerName, partitionKeyPath) + .WithClientEncryptionPolicy(2) + .WithIncludedPath(path1) + .Attach() + .CreateAsync(); + + Assert.Fail("CreateCollection with invalid ClientEncryptionPolicy should have failed."); + } + catch (ArgumentException ex) + { + Assert.IsTrue(ex.Message.Contains("Path: /users which is part of the partition key has to be encrypted with Deterministic type Encryption."), ex.Message); + } + + // invalid encryption type for id encryption + path1.Path = "/id"; + try + { + ContainerResponse containerResponse = await this.database.DefineContainer(containerName, partitionKeyPath) + .WithClientEncryptionPolicy(2) + .WithIncludedPath(path1) + .Attach() + .CreateAsync(); + + Assert.Fail("CreateCollection with invalid ClientEncryptionPolicy should have failed."); + } + catch (ArgumentException ex) + { + Assert.IsTrue(ex.Message.Contains("Only Deterministic encryption type is supported for path: /id."), ex.Message); + } } private bool VerifyClientEncryptionIncludedPath(ClientEncryptionIncludedPath expected, ClientEncryptionIncludedPath actual) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json index aa8403d330..f8cb3adf8e 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Contracts/DotNetSDKAPI.json @@ -996,10 +996,10 @@ ], "MethodInfo": "System.Collections.Generic.IEnumerable`1[Microsoft.Azure.Cosmos.ClientEncryptionIncludedPath] IncludedPaths;CanRead:True;CanWrite:True;System.Collections.Generic.IEnumerable`1[Microsoft.Azure.Cosmos.ClientEncryptionIncludedPath] get_IncludedPaths();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "Void .ctor(System.Collections.Generic.IEnumerable`1[Microsoft.Azure.Cosmos.ClientEncryptionIncludedPath])": { + "Void .ctor(System.Collections.Generic.IEnumerable`1[Microsoft.Azure.Cosmos.ClientEncryptionIncludedPath], Int32)": { "Type": "Constructor", "Attributes": [], - "MethodInfo": "[Void .ctor(System.Collections.Generic.IEnumerable`1[Microsoft.Azure.Cosmos.ClientEncryptionIncludedPath]), Void .ctor(System.Collections.Generic.IEnumerable`1[Microsoft.Azure.Cosmos.ClientEncryptionIncludedPath])]" + "MethodInfo": "[Void .ctor(System.Collections.Generic.IEnumerable`1[Microsoft.Azure.Cosmos.ClientEncryptionIncludedPath], Int32), Void .ctor(System.Collections.Generic.IEnumerable`1[Microsoft.Azure.Cosmos.ClientEncryptionIncludedPath], Int32)]" } }, "NestedTypes": {} @@ -4176,10 +4176,10 @@ "Attributes": [], "MethodInfo": "Microsoft.Azure.Cosmos.ContainerProperties Build();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, - "Microsoft.Azure.Cosmos.Fluent.ClientEncryptionPolicyDefinition WithClientEncryptionPolicy()": { + "Microsoft.Azure.Cosmos.Fluent.ClientEncryptionPolicyDefinition WithClientEncryptionPolicy(Int32)": { "Type": "Method", "Attributes": [], - "MethodInfo": "Microsoft.Azure.Cosmos.Fluent.ClientEncryptionPolicyDefinition WithClientEncryptionPolicy();IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" + "MethodInfo": "Microsoft.Azure.Cosmos.Fluent.ClientEncryptionPolicyDefinition WithClientEncryptionPolicy(Int32);IsAbstract:False;IsStatic:False;IsVirtual:False;IsGenericMethod:False;IsConstructor:False;IsFinal:False;" }, "Microsoft.Azure.Cosmos.Fluent.ConflictResolutionDefinition WithConflictResolution()": { "Type": "Method",