Skip to content

Commit

Permalink
Client Encryption: Adds fix to allow partition key path and id to be …
Browse files Browse the repository at this point in the history
…part of client encryption policy. (#3211)

* Support PK and Id encryption. Bump up policy format version.

* Update DotNetSDKAPI.json

* Update CosmosContainerTests.cs

* fixes as per review comments. Added float pk constructor.

* Update ClientEncryptionPolicy.cs

* fixed exception message.

* Update ClientEncryptionPolicy.cs

* fixes, policy format in client encryption policy definition.

* get raw partition key values from partition key list.

* Update CosmosContainerTests.cs

* Fixes as per review request.

* fixed contracts.

* Fixes as per review comments.

Co-authored-by: Matias Quaranta <ealsur@users.noreply.github.com>
  • Loading branch information
kr-santosh and ealsur authored Jun 9, 2022
1 parent dbbae92 commit 01e5b29
Show file tree
Hide file tree
Showing 7 changed files with 484 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,55 @@ namespace Microsoft.Azure.Cosmos.Fluent

/// <summary>
/// <see cref="ClientEncryptionPolicy"/> fluent definition.
/// The <see cref="ClientEncryptionPolicy"/> 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 <see cref="ClientEncryptionIncludedPath"/> have to be JSON strings.
/// </summary>
/// <example>
/// This example shows how to create a <see cref="ClientEncryptionPolicy"/> using <see cref="ClientEncryptionPolicyDefinition"/>.
/// <code language="c#">
/// <![CDATA[
/// ClientEncryptionIncludedPath path1 = new ClientEncryptionIncludedPath()
/// {
/// Path = partitionKeyPath,
/// ClientEncryptionKeyId = "key1",
/// EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256",
/// EncryptionType = "Deterministic"
/// };
///
/// ClientEncryptionIncludedPath path2 = new ClientEncryptionIncludedPath()
/// {
/// Path = "/id",
/// ClientEncryptionKeyId = "key2",
/// EncryptionAlgorithm = "AEAD_AES_256_CBC_HMAC_SHA256",
/// EncryptionType = "Deterministic"
/// };
///
/// ContainerResponse containerResponse = await this.database.DefineContainer(containerName, partitionKeyPath)
/// .WithClientEncryptionPolicy(policyFormatVersion:2)
/// .WithIncludedPath(path1)
/// .WithIncludedPath(path2)
/// .Attach()
/// .CreateAsync()
/// };
/// ]]>
/// </code>
/// </example>
public sealed class ClientEncryptionPolicyDefinition
{
private readonly Collection<ClientEncryptionIncludedPath> clientEncryptionIncludedPaths = new Collection<ClientEncryptionIncludedPath>();
private readonly ContainerBuilder parent;
private readonly Action<ClientEncryptionPolicy> attachCallback;
private readonly int policyFormatVersion;

internal ClientEncryptionPolicyDefinition(
ContainerBuilder parent,
Action<ClientEncryptionPolicy> attachCallback)
Action<ClientEncryptionPolicy> 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;
}

/// <summary>
Expand All @@ -41,7 +77,7 @@ public ClientEncryptionPolicyDefinition WithIncludedPath(ClientEncryptionInclude
/// <returns>An instance of the parent.</returns>
public ContainerBuilder Attach()
{
this.attachCallback(new ClientEncryptionPolicy(this.clientEncryptionIncludedPaths));
this.attachCallback(new ClientEncryptionPolicy(this.clientEncryptionIncludedPaths, this.policyFormatVersion));
return this.parent;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,14 @@ ChangeFeedPolicyDefinition WithChangeFeedPolicy(TimeSpan retention)
/// <summary>
/// Defines the ClientEncryptionPolicy for Azure Cosmos container
/// </summary>
/// <param name="policyFormatVersion">Version of the client encryption policy definition. Current supported versions are 1 and 2. Default version is 1.</param>
/// <returns>An instance of <see cref="ClientEncryptionPolicyDefinition"/>.</returns>
public ClientEncryptionPolicyDefinition WithClientEncryptionPolicy()
public ClientEncryptionPolicyDefinition WithClientEncryptionPolicy(int policyFormatVersion = 1)
{
return new ClientEncryptionPolicyDefinition(
this,
(clientEncryptionPolicy) => this.AddClientEncryptionPolicy(clientEncryptionPolicy));
(clientEncryptionPolicy) => this.AddClientEncryptionPolicy(clientEncryptionPolicy),
policyFormatVersion);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,53 @@ namespace Microsoft.Azure.Cosmos
using Newtonsoft.Json.Linq;

/// <summary>
/// Client encryption policy.
/// The <see cref="ClientEncryptionPolicy"/> 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.
/// </summary>
/// <example>
/// This example shows how to create a <see cref="ClientEncryptionPolicy"/>.
/// <code language="c#">
/// <![CDATA[
/// Collection<ClientEncryptionIncludedPath> paths = new Collection<ClientEncryptionIncludedPath>()
/// {
/// 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)
/// };
/// ]]>
/// </code>
/// </example>
public sealed class ClientEncryptionPolicy
{
/// <summary>
/// Initializes a new instance of the <see cref="ClientEncryptionPolicy"/> class.
/// </summary>
/// <param name="includedPaths">List of paths to include in the policy definition.</param>
public ClientEncryptionPolicy(IEnumerable<ClientEncryptionIncludedPath> includedPaths)
/// <param name="policyFormatVersion"> Version of the client encryption policy definition. Current supported versions are 1 and 2. Default version is 1. </param>
public ClientEncryptionPolicy(IEnumerable<ClientEncryptionIncludedPath> 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]
Expand All @@ -33,7 +67,7 @@ private ClientEncryptionPolicy()
}

/// <summary>
/// Paths of the item that need encryption along with path-specific settings.
/// Paths of the item that need encryption along with path-specific settings.
/// </summary>
[JsonProperty(PropertyName = "includedPaths")]
public IEnumerable<ClientEncryptionIncludedPath> IncludedPaths
Expand All @@ -55,43 +89,60 @@ public IEnumerable<ClientEncryptionIncludedPath> IncludedPaths
internal IDictionary<string, JToken> AdditionalProperties { get; private set; }

/// <summary>
/// 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.
/// </summary>
/// <param name="partitionKeyPathTokens">Tokens corresponding to validated partition key.</param>
internal void ValidatePartitionKeyPathsAreNotEncrypted(IReadOnlyList<IReadOnlyList<string>> partitionKeyPathTokens)
internal void ValidatePartitionKeyPathsIfEncrypted(IReadOnlyList<IReadOnlyList<string>> partitionKeyPathTokens)
{
Debug.Assert(partitionKeyPathTokens != null);
IEnumerable<string> propertiesToEncrypt = this.IncludedPaths.Select(p => p.Path.Substring(1));

foreach (IReadOnlyList<string> 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<ClientEncryptionIncludedPath> 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> clientEncryptionIncludedPath)
private static void ValidateIncludedPaths(
IEnumerable<ClientEncryptionIncludedPath> clientEncryptionIncludedPath,
int policyFormatVersion)
{
List<string> includedPathsList = new List<string>();
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)
{
Expand All @@ -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}'.");
}
Expand All @@ -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"))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ internal void ValidateRequiredProperties()

if (this.ClientEncryptionPolicy != null)
{
this.ClientEncryptionPolicy.ValidatePartitionKeyPathsAreNotEncrypted(this.PartitionKeyPathTokens);
this.ClientEncryptionPolicy.ValidatePartitionKeyPathsIfEncrypted(this.PartitionKeyPathTokens);
}
}
}
Expand Down
Loading

0 comments on commit 01e5b29

Please sign in to comment.