Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Azure.DataApiBuilder.Service.Parsers;
using Azure.DataApiBuilder.Service.Resolvers;
using Azure.DataApiBuilder.Service.Tests.SqlTests;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down Expand Up @@ -99,6 +100,50 @@ public void VisitorLeftFieldGreaterThanRightNullFilterTest()
);
}

[DataTestMethod]
// Constant on left side and OData EDM object on right side of binary operator. (L->R)
[DataRow("'1' eq int_types", false, DisplayName = "L->R: Cast token claim of type string to integer, left to right ")]
[DataRow("'13B4F4EC-C45B-46EC-99F2-77BC22A256A7' eq guid_types", false, DisplayName = "L->R: Cast token claim of type string to GUID")]
[DataRow("'true' eq boolean_types", false, DisplayName = "L->R: Cast token claim of type string to bool (true)")]
[DataRow("'false' eq boolean_types", false, DisplayName = "L->R: Cast token claim of type string to bool (false)")]
[DataRow("1 eq string_types", false, DisplayName = "L->R: Cast token claim of type int to string")]
[DataRow("true eq string_types", false, DisplayName = "L->R: Cast token claim of type bool to string")]
// Constant on right side and OData EDM object on left side of binary operator. (R->L)
[DataRow("int_types eq '1'", false, DisplayName = "R->L: Cast token claim of type string to integer")]
[DataRow("guid_types eq '13B4F4EC-C45B-46EC-99F2-77BC22A256A7'", false, DisplayName = "R->L: Cast token claim of type string to GUID")]
[DataRow("boolean_types eq 'true'", false, DisplayName = "R->L: Cast token claim of type string to bool (true)")]
[DataRow("boolean_types eq 'false'", false, DisplayName = "R->L: Cast token claim of type string to bool (false)")]
[DataRow("string_types eq 1", false, DisplayName = "R->L: Cast token claim of type int to string")]
[DataRow("string_types eq true", false, DisplayName = "R->L: Cast token claim of type bool to string")]
// Comparisons expected to fail due to inability to cast
[DataRow("boolean_types eq 2", true, DisplayName = "Fail to cast arbitrary int to bool")]
[DataRow("guid_types eq 1", true, DisplayName = "Fail to cast arbitrary int to GUID")]
[DataRow("guid_types eq 'stringConstant'", true, DisplayName = "Fail to cast arbitrary string to GUID")]
public void CustomODataUriParserResolverTest(string resolvedAuthZPolicyText, bool errorExpected)
{
// Arrange
string entityName = "SupportedType";
string tableName = "type_table";
string filterQueryString = "?$filter=" + resolvedAuthZPolicyText;
string expectedErrorMessageFragment = "A binary operator with incompatible types was detected.";

//Act + Assert
try
{
FilterClause ast = _sqlMetadataProvider
.GetODataParser()
.GetFilterClause(
filterQueryString,
resourcePath: $"{entityName}.{DEFAULT_SCHEMA_NAME}.{tableName}",
customResolver: new ClaimsTypeDataUriResolver());
Assert.IsFalse(errorExpected, message: "Filter clause creation was expected to fail.");
}
catch (Exception e) when (e is DataApiBuilderException || e is ODataException)
{
Assert.IsTrue(errorExpected, message: "Filter clause creation was not expected to fail.");
Assert.IsTrue(e.Message.Contains(expectedErrorMessageFragment));
}
}
#endregion
#region Negative Tests
/// <summary>
Expand Down
94 changes: 94 additions & 0 deletions src/Service/Parsers/ClaimsTypeDataUriResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;

namespace Azure.DataApiBuilder.Service.Parsers
{
/// <summary>
/// Custom OData Resolver which attempts to assist with processing resolved token claims
/// within an authorization policy string that will be used to create an OData filter clause.
/// This resolver's type coercion is meant to be utilized for authorization policy processing
/// and NOT URL query string processing.
/// </summary>
/// <seealso cref="https://devblogs.microsoft.com/odata/tutorial-sample-odatauriparser-extension-support/#write-customized-extensions-from-scratch"/>
public class ClaimsTypeDataUriResolver : ODataUriResolver
{
/// <summary>
/// Between two nodes in the filter clause, determine the:
/// - PrimaryOperand: Node representing an OData EDM model object and has Kind == QueryNodeKind.SingleValuePropertyAccess.
/// - OperandToConvert: Node representing a constant value and has kind QueryNodeKind.Constant.
/// This resolver will overwrite the OperandToConvert node to a new ConstantNode where the value type is that of the PrimaryOperand node.
/// </summary>
/// <param name="binaryOperatorKind">the operator kind</param>
/// <param name="leftNode">the left operand</param>
/// <param name="rightNode">the right operand</param>
/// <param name="typeReference">type reference for the result BinaryOperatorNode.</param>
public override void PromoteBinaryOperandTypes(BinaryOperatorKind binaryOperatorKind, ref SingleValueNode leftNode, ref SingleValueNode rightNode, out IEdmTypeReference typeReference)
{
if (leftNode.TypeReference.PrimitiveKind() != rightNode.TypeReference.PrimitiveKind())
{
if ((leftNode.Kind == QueryNodeKind.SingleValuePropertyAccess) && (rightNode is ConstantNode))
{
TryConvertNodeToTargetType(
targetType: leftNode.TypeReference.PrimitiveKind(),
operandToConvert: ref rightNode
);
}
else if (rightNode.Kind == QueryNodeKind.SingleValuePropertyAccess && leftNode is ConstantNode)
{
TryConvertNodeToTargetType(
targetType: rightNode.TypeReference.PrimitiveKind(),
operandToConvert: ref leftNode
);
}
}

base.PromoteBinaryOperandTypes(binaryOperatorKind, ref leftNode, ref rightNode, out typeReference);
}

/// <summary>
/// Uses type specific parsers to attempt converting the supplied node to a new ConstantNode of type targetType.
/// </summary>
/// <param name="targetType">Primitive type (string, bool, int, etc.) of the primary node's value.</param>
/// <param name="operandToConvert">Node representing a constant value which should be converted to a ConstantNode of type targetType.</param>
private static void TryConvertNodeToTargetType(EdmPrimitiveTypeKind targetType, ref SingleValueNode operandToConvert)
{
ConstantNode? preConvertedConstant = operandToConvert as ConstantNode;

if (preConvertedConstant?.Value is null)
{
return;
}

if (targetType == EdmPrimitiveTypeKind.Int32)
{
if (int.TryParse(preConvertedConstant.Value.ToString(), out int result))
{
operandToConvert = new ConstantNode(constantValue: result);
}
}
else if (targetType == EdmPrimitiveTypeKind.String)
{
string? objectValue = preConvertedConstant.Value.ToString();
if (objectValue is not null)
{
operandToConvert = new ConstantNode(constantValue: objectValue);
}
}
else if (targetType == EdmPrimitiveTypeKind.Boolean)
{
if (bool.TryParse(preConvertedConstant.Value.ToString(), out bool result))
{
operandToConvert = new ConstantNode(constantValue: result);
}
}
else if (targetType == EdmPrimitiveTypeKind.Guid)
{
if (Guid.TryParse(preConvertedConstant.Value.ToString(), out Guid result))
{
operandToConvert = new ConstantNode(constantValue: result);
}
}
}
}
}
4 changes: 3 additions & 1 deletion src/Service/Parsers/EdmModelBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ SourceDefinition sourceDefinition
switch (columnSystemType.Name)
{
case "String":
case "Guid":
type = EdmPrimitiveTypeKind.String;
break;
case "Guid":
type = EdmPrimitiveTypeKind.Guid;
break;
case "Byte":
type = EdmPrimitiveTypeKind.Byte;
break;
Expand Down
9 changes: 8 additions & 1 deletion src/Service/Parsers/FilterParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ public void BuildModel(ISqlMetadataProvider sqlMetadataProvider)
/// </summary>
/// <param name="filterQueryString">Represents the $filter part of the query string</param>
/// <param name="resourcePath">Represents the resource path, in our case the entity name.</param>
/// <param name="customResolver">ODataUriResolver resolving different kinds of Uri parsing context.</param>
/// <returns>An AST FilterClause that represents the filter portion of the WHERE clause.</returns>
public FilterClause GetFilterClause(string filterQueryString, string resourcePath)
public FilterClause GetFilterClause(string filterQueryString, string resourcePath, ODataUriResolver? customResolver = null)
{
if (_model == null)
{
Expand All @@ -45,6 +46,12 @@ public FilterClause GetFilterClause(string filterQueryString, string resourcePat
{
Uri relativeUri = new(resourcePath + '/' + filterQueryString, UriKind.Relative);
ODataUriParser parser = new(_model!, relativeUri);

if (customResolver is not null)
{
parser.Resolver = customResolver;
}

return parser.ParseFilter();
}
catch (ODataException e)
Expand Down
5 changes: 4 additions & 1 deletion src/Service/Resolvers/AuthorizationPolicyHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ public static void ProcessAuthorizationPolicies(

// Parse and save the values that are needed to later generate SQL query predicates
// FilterClauseInDbPolicy is an Abstract Syntax Tree representing the parsed policy text.
return sqlMetadataProvider.GetODataParser().GetFilterClause(dbPolicyClause, $"{entityName}.{resourcePath}");
return sqlMetadataProvider.GetODataParser().GetFilterClause(
filterQueryString: dbPolicyClause,
resourcePath: $"{entityName}.{resourcePath}",
customResolver: new ClaimsTypeDataUriResolver());
}

return null;
Expand Down
5 changes: 4 additions & 1 deletion src/Service/Services/RestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,10 @@ RuntimeConfigProvider runtimeConfigProvider

// Parse and save the values that are needed to later generate queries in the given RestRequestContext.
// DbPolicyClause is an Abstract Syntax Tree representing the parsed policy text.
context.DbPolicyClause = _sqlMetadataProvider.GetODataParser().GetFilterClause(dbPolicy, $"{context.EntityName}.{context.DatabaseObject.FullName}");
context.DbPolicyClause = _sqlMetadataProvider.GetODataParser().GetFilterClause(
filterQueryString: dbPolicy,
resourcePath: $"{context.EntityName}.{context.DatabaseObject.FullName}",
customResolver: new ClaimsTypeDataUriResolver());
}

// At this point for DELETE, the primary key should be populated in the Request Context.
Expand Down