diff --git a/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs b/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs
index 115d926661..66876e6456 100644
--- a/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs
+++ b/src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs
@@ -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;
@@ -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
///
diff --git a/src/Service/Parsers/ClaimsTypeDataUriResolver.cs b/src/Service/Parsers/ClaimsTypeDataUriResolver.cs
new file mode 100644
index 0000000000..672635a940
--- /dev/null
+++ b/src/Service/Parsers/ClaimsTypeDataUriResolver.cs
@@ -0,0 +1,94 @@
+using System;
+using Microsoft.OData.Edm;
+using Microsoft.OData.UriParser;
+
+namespace Azure.DataApiBuilder.Service.Parsers
+{
+ ///
+ /// 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.
+ ///
+ ///
+ public class ClaimsTypeDataUriResolver : ODataUriResolver
+ {
+ ///
+ /// 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.
+ ///
+ /// the operator kind
+ /// the left operand
+ /// the right operand
+ /// type reference for the result BinaryOperatorNode.
+ 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);
+ }
+
+ ///
+ /// Uses type specific parsers to attempt converting the supplied node to a new ConstantNode of type targetType.
+ ///
+ /// Primitive type (string, bool, int, etc.) of the primary node's value.
+ /// Node representing a constant value which should be converted to a ConstantNode of type targetType.
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Service/Parsers/EdmModelBuilder.cs b/src/Service/Parsers/EdmModelBuilder.cs
index 9383ca4aef..ea07f87a7b 100644
--- a/src/Service/Parsers/EdmModelBuilder.cs
+++ b/src/Service/Parsers/EdmModelBuilder.cs
@@ -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;
diff --git a/src/Service/Parsers/FilterParser.cs b/src/Service/Parsers/FilterParser.cs
index d3177a84f4..c800f0e15e 100644
--- a/src/Service/Parsers/FilterParser.cs
+++ b/src/Service/Parsers/FilterParser.cs
@@ -29,8 +29,9 @@ public void BuildModel(ISqlMetadataProvider sqlMetadataProvider)
///
/// Represents the $filter part of the query string
/// Represents the resource path, in our case the entity name.
+ /// ODataUriResolver resolving different kinds of Uri parsing context.
/// An AST FilterClause that represents the filter portion of the WHERE clause.
- public FilterClause GetFilterClause(string filterQueryString, string resourcePath)
+ public FilterClause GetFilterClause(string filterQueryString, string resourcePath, ODataUriResolver? customResolver = null)
{
if (_model == null)
{
@@ -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)
diff --git a/src/Service/Resolvers/AuthorizationPolicyHelpers.cs b/src/Service/Resolvers/AuthorizationPolicyHelpers.cs
index 46417b4210..0761e47a05 100644
--- a/src/Service/Resolvers/AuthorizationPolicyHelpers.cs
+++ b/src/Service/Resolvers/AuthorizationPolicyHelpers.cs
@@ -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;
diff --git a/src/Service/Services/RestService.cs b/src/Service/Services/RestService.cs
index 51841ed808..fab1ec867b 100644
--- a/src/Service/Services/RestService.cs
+++ b/src/Service/Services/RestService.cs
@@ -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.