Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expression validation #311

Merged
merged 18 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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
17 changes: 16 additions & 1 deletion src/Altinn.App.Api/Controllers/ResourceController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,23 @@ public async Task<ActionResult> GetFooterLayout(string org, string app)
{
return NoContent();
}

return Ok(layout);
}

/// <summary>
/// Get validation configuration file.
/// </summary>
/// <param name="org">The application owner short name</param>
/// <param name="app">The application name</param>
/// <param name="id">Unique identifier of the model to fetch validations for.</param>
/// <returns>The validation configuration file as json.</returns>
[HttpGet]
[Route("{org}/{app}/api/validationconfig/{id}")]
public ActionResult GetValidationConfiguration(string org, string app, string id)
{
string? validationConfiguration = _appResourceService.GetValidationConfiguration(id);
return Ok(validationConfiguration);
}
}
}
12 changes: 11 additions & 1 deletion src/Altinn.App.Core/Configuration/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public class AppSettings
/// </summary>
public const string JSON_SCHEMA_FILENAME = "schema.json";

/// <summary>
/// Constant for the location of validation configuration file
/// </summary>
public const string VALIDATION_CONFIG_FILENAME = "validation.json";

/// <summary>
/// The app configuration baseUrl where files are stored in the container
/// </summary>
Expand Down Expand Up @@ -83,7 +88,7 @@ public class AppSettings
/// </summary>
public string LayoutSetsFileName { get; set; } = "layout-sets.json";

/// <summary>
/// <summary>
/// Gets or sets the name of the layout setting file name
/// </summary>
public string FooterFileName { get; set; } = "footer.json";
Expand All @@ -103,6 +108,11 @@ public class AppSettings
/// </summary>
public string JsonSchemaFileName { get; set; } = JSON_SCHEMA_FILENAME;

/// <summary>
/// Gets or sets The JSON schema file name
/// </summary>
public string ValidationConfigurationFileName { get; set; } = VALIDATION_CONFIG_FILENAME;

/// <summary>
/// Gets or sets the filename for application meta data
/// </summary>
Expand Down
275 changes: 275 additions & 0 deletions src/Altinn.App.Core/Features/Validation/ExpressionValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
using System.Text.Json;
using Altinn.App.Core.Helpers;
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.Expressions;
using Altinn.App.Core.Models.Validation;
using Microsoft.Extensions.Logging;


namespace Altinn.App.Core.Features.Validation
{
/// <summary>
/// Validates form data against expression validations
/// </summary>
public static class ExpressionValidator
{
/// <inheritdoc />
public static IEnumerable<ValidationIssue> Validate(string dataType, IAppResources appResourceService, IDataModelAccessor dataModel, LayoutEvaluatorState evaluatorState, ILogger logger)
{
var rawValidationConfig = appResourceService.GetValidationConfiguration(dataType);
if (rawValidationConfig == null)
{
// No validation configuration exists for this data type
return new List<ValidationIssue>();
}

var validationConfig = JsonDocument.Parse(rawValidationConfig).RootElement;
return Validate(validationConfig, dataModel, evaluatorState, logger);
}

/// <inheritdoc />
public static IEnumerable<ValidationIssue> Validate(JsonElement validationConfig, IDataModelAccessor dataModel, LayoutEvaluatorState evaluatorState, ILogger logger)
{
var validationIssues = new List<ValidationIssue>();
var expressionValidations = ParseExpressionValidationConfig(validationConfig, logger);
foreach (var validationObject in expressionValidations)
{
var baseField = validationObject.Key;
var resolvedFields = dataModel.GetResolvedKeys(baseField);
var validations = validationObject.Value;
foreach (var resolvedField in resolvedFields)
{
var positionalArguments = new[] { resolvedField };
foreach (var validation in validations)
{
try
{
if (validation.Condition == null)
{
continue;
}

var isInvalid = ExpressionEvaluator.EvaluateExpression(evaluatorState, validation.Condition, null, positionalArguments);
if (isInvalid is not bool)
{
throw new ArgumentException($"Validation condition for {resolvedField} did not evaluate to a boolean");
}
if ((bool)isInvalid)
{
var validationIssue = new ValidationIssue
{
Field = resolvedField,
Severity = validation.Severity ?? ValidationIssueSeverity.Error,
CustomTextKey = validation.Message,
Code = validation.Message,
Source = "Expression" // TODO: Add source to ValidationIssueSources
};
validationIssues.Add(validationIssue);
}
}
catch (Exception e)
{
logger.LogError($"Error while evaluating expression validation for {resolvedField}: {e}");
}
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved
}
}
}


return validationIssues;
}

private static RawExpressionValidation? ResolveValidationDefinition(string name, JsonElement definition, Dictionary<string, RawExpressionValidation> resolvedDefinitions, ILogger logger)
{
var resolvedDefinition = new RawExpressionValidation();
var rawDefinition = definition.Deserialize<RawExpressionValidation>(new JsonSerializerOptions
{
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
if (rawDefinition == null)
{
logger.LogError($"Validation definition {name} could not be parsed");
return null;
}
if (rawDefinition.Ref != null)
{
var reference = resolvedDefinitions.GetValueOrDefault(rawDefinition.Ref);
if (reference == null)
{
logger.LogError($"Could not resolve reference {rawDefinition.Ref} for validation {name}");
return null;

}
resolvedDefinition.Message = reference.Message;
resolvedDefinition.Condition = reference.Condition;
resolvedDefinition.Severity = reference.Severity;
}

if (rawDefinition.Message != null)
{
resolvedDefinition.Message = rawDefinition.Message;
}

if (rawDefinition.Condition != null)
{
resolvedDefinition.Condition = rawDefinition.Condition;
}

if (rawDefinition.Severity != null)
{
resolvedDefinition.Severity = rawDefinition.Severity;
}

if (resolvedDefinition.Message == null)
{
logger.LogError($"Validation {name} is missing message");
return null;
}

if (resolvedDefinition.Condition == null)
{
logger.LogError($"Validation {name} is missing condition");
return null;
}

return resolvedDefinition;
}

private static ExpressionValidation? ResolveExpressionValidation(string field, JsonElement definition, Dictionary<string, RawExpressionValidation> resolvedDefinitions, ILogger logger)
{

var rawExpressionValidatıon = new RawExpressionValidation();

if (definition.ValueKind == JsonValueKind.String)
{
var stringReference = definition.GetString();
if (stringReference == null)
{
logger.LogError($"Could not resolve null reference for validation for field {field}");
return null;
}
var reference = resolvedDefinitions.GetValueOrDefault(stringReference);
if (reference == null)
{
logger.LogError($"Could not resolve reference {stringReference} for validation for field {field}");
return null;
}
rawExpressionValidatıon.Message = reference.Message;
rawExpressionValidatıon.Condition = reference.Condition;
rawExpressionValidatıon.Severity = reference.Severity;
}
else
{
var expressionDefinition = definition.Deserialize<RawExpressionValidation>(new JsonSerializerOptions
{
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
if (expressionDefinition == null)
{
logger.LogError($"Validation for field {field} could not be parsed");
return null;
}

if (expressionDefinition.Ref != null)
{
var reference = resolvedDefinitions.GetValueOrDefault(expressionDefinition.Ref);
if (reference == null)
{
logger.LogError($"Could not resolve reference {expressionDefinition.Ref} for validation for field {field}");
return null;

}
rawExpressionValidatıon.Message = reference.Message;
rawExpressionValidatıon.Condition = reference.Condition;
rawExpressionValidatıon.Severity = reference.Severity;
}

if (expressionDefinition.Message != null)
{
rawExpressionValidatıon.Message = expressionDefinition.Message;
}

if (expressionDefinition.Condition != null)
{
rawExpressionValidatıon.Condition = expressionDefinition.Condition;
}

if (expressionDefinition.Severity != null)
{
rawExpressionValidatıon.Severity = expressionDefinition.Severity;
}
}

if (rawExpressionValidatıon.Message == null)
{
logger.LogError($"Validation for field {field} is missing message");
return null;
}

if (rawExpressionValidatıon.Condition == null)
{
logger.LogError($"Validation for field {field} is missing condition");
return null;
}

var expressionValidation = new ExpressionValidation
{
Message = rawExpressionValidatıon.Message,
Condition = rawExpressionValidatıon.Condition,
Severity = rawExpressionValidatıon.Severity ?? ValidationIssueSeverity.Error,
};

return expressionValidation;
}

private static Dictionary<string, List<ExpressionValidation>> ParseExpressionValidationConfig(JsonElement expressionValidationConfig, ILogger logger)
{
var expressionValidationDefinitions = new Dictionary<string, RawExpressionValidation>();
JsonElement definitionsObject;
var hasDefinitions = expressionValidationConfig.TryGetProperty("definitions", out definitionsObject);
if (hasDefinitions)
{
foreach (var definitionObject in definitionsObject.EnumerateObject())
{
var name = definitionObject.Name;
var definition = definitionObject.Value;
var resolvedDefinition = ResolveValidationDefinition(name, definition, expressionValidationDefinitions, logger);
if (resolvedDefinition == null)
{
logger.LogError($"Validation definition {name} could not be resolved");
continue;
}
expressionValidationDefinitions[name] = resolvedDefinition;
}
}
var expressionValidations = new Dictionary<string, List<ExpressionValidation>>();
JsonElement validationsObject;
var hasValidations = expressionValidationConfig.TryGetProperty("validations", out validationsObject);
if (hasValidations)
{
foreach (var validationArray in validationsObject.EnumerateObject())
{
var field = validationArray.Name;
var validations = validationArray.Value;
foreach (var validation in validations.EnumerateArray())
{
if (!expressionValidations.ContainsKey(field))
{
expressionValidations[field] = new List<ExpressionValidation>();
}
var resolvedExpressionValidation = ResolveExpressionValidation(field, validation, expressionValidationDefinitions, logger);
if (resolvedExpressionValidation == null)
{
logger.LogError($"Validation for field {field} could not be resolved");
continue;
}
expressionValidations[field].Add(resolvedExpressionValidation);
}
}
}
return expressionValidations;
}
}
}
6 changes: 6 additions & 0 deletions src/Altinn.App.Core/Features/Validation/ValidationAppSI.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Helpers.DataModel;
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.AppModel;
using Altinn.App.Core.Internal.Data;
Expand Down Expand Up @@ -237,6 +238,10 @@ public async Task<List<ValidationIssue>> ValidateDataElement(Instance instance,
// is respected on groups
var layoutErrors = LayoutEvaluator.RunLayoutValidationsForRequired(evaluationState, dataElement.Id);
messages.AddRange(layoutErrors);

// Run expression validations
var expressionErrors = ExpressionValidator.Validate(dataType.Id, _appResourcesService, new DataModel(data), evaluationState, _logger);
messages.AddRange(expressionErrors);
}

// run Standard mvc validation using the System.ComponentModel.DataAnnotations
Expand All @@ -257,6 +262,7 @@ public async Task<List<ValidationIssue>> ValidateDataElement(Instance instance,
{
messages.AddRange(MapModelStateToIssueList(actionContext.ModelState, instance, dataElement.Id, data.GetType()));
}

}

return messages;
Expand Down
Loading