fixes, improvements

This commit is contained in:
2025-10-25 09:03:51 +02:00
parent cc7fe89330
commit fd0bdced11
24 changed files with 490 additions and 271 deletions

4
certmgr/AssemblyInfo.cs Normal file
View File

@@ -0,0 +1,4 @@
using System.Reflection;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("certmgrTest")]

View File

@@ -34,9 +34,9 @@ public sealed class CertificateSettings
public X509KeyUsageFlags KeyUsage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public Task<ValidationResults> ValidateAsync(CancellationToken cancellationToken)
public Task<IValidationResult> ValidateAsync(CancellationToken cancellationToken)
{
ValidationResults results = new ValidationResults();
ValidationResults results = new ValidationResults(nameof(CertificateSettings));
if (string.IsNullOrEmpty(SubjectName))
{
@@ -48,7 +48,7 @@ public sealed class CertificateSettings
results.AddInvalid(nameof(ValidityPeriod), "must be greater than 1sec");
}
return Task.FromResult(results);
return Task.FromResult((IValidationResult)results);
}
public override string ToString()

View File

@@ -6,7 +6,7 @@ using CertMgr.Core.Validation;
namespace CertMgr.CertGen.Utils;
public sealed class SubjectValidator : ISettingValidator<string>
public sealed class SubjectValidator : IValueValidator<string>
{
// the list contains most used attributes, but more of them exists. Extend the list if needed.
private static readonly HashSet<string> AllowedAttributes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
@@ -19,16 +19,16 @@ public sealed class SubjectValidator : ISettingValidator<string>
public SubjectValidator(string settingName)
{
SettingName = settingName;
ValueName = settingName;
}
public string SettingName { [DebuggerStepThrough] get; }
public string ValueName { [DebuggerStepThrough] get; }
public Task<ValidationResult> ValidateAsync(string? settingValue, CancellationToken cancellationToken)
public Task<IValidationResult> ValidateAsync(string? settingValue, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(settingValue))
{
return Task.FromResult(new ValidationResult(SettingName, false, "must not be null"));
return Task.FromResult((IValidationResult)new ValidationResult(ValueName, false, "must not be null"));
}
try
@@ -39,18 +39,18 @@ public sealed class SubjectValidator : ISettingValidator<string>
if (!EnsureAllowedAttributes(normalizedSubject, out string? error))
{
return Task.FromResult(new ValidationResult(SettingName, false, error));
return Task.FromResult((IValidationResult)new ValidationResult(ValueName, false, error));
}
return Task.FromResult(new ValidationResult(SettingName, true, "success"));
return Task.FromResult((IValidationResult)new ValidationResult(ValueName, true, "success"));
}
catch (Exception e)
{
return Task.FromResult(new ValidationResult(SettingName, false, "invalid value: '{0}'. Exception = {1}: {2}", settingValue ?? "<null>", e.GetType().Name, e.Message));
return Task.FromResult((IValidationResult)new ValidationResult(ValueName, false, "invalid value: '{0}'. Exception = {1}: {2}", settingValue ?? "<null>", e.GetType().Name, e.Message));
}
}
public Task<ValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken)
public Task<IValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken)
{
return ValidateAsync(value as string, cancellationToken);
}
@@ -76,7 +76,7 @@ public sealed class SubjectValidator : ISettingValidator<string>
break;
}
// reached start or RDN (relative distinguished name)
// reached start of RDN (relative distinguished name)
// note that single RDN might contain multiple values (rare cases), in such a case they are split by '+' (e.g. 'CN=pankrac + OU=machinists')
while (currentIndex < totalLength)
{
@@ -161,7 +161,7 @@ public sealed class SubjectValidator : ISettingValidator<string>
continue;
}
// end of RDN, break to proceed with outer while-loop on next RDN (if available)
// end of RDN - break inner while-loop to proceed with outer while-loop on next RDN (if available)
if (currentIndex < totalLength && normalizedSubject[currentIndex] == ',')
{
currentIndex++;
@@ -240,7 +240,7 @@ public sealed class SubjectValidator : ISettingValidator<string>
if (firstArc < 2 && secondArc > 39)
{
errorMessage = string.Format("If first arc has value < 2 then second arc must be from range [0..39] (first arc value = '{0}', second arc value = '{1}')", firstArc, secondArc);
errorMessage = string.Format("If first arc has value < 2 then second arc must be from range [0;39] (first arc value = '{0}', second arc value = '{1}')", firstArc, secondArc);
return false;
}

View File

@@ -8,6 +8,7 @@ public sealed class SettingAttribute : Attribute
{
Name = name;
IsMandatory = false;
IsSecret = false;
AlternateNames = Array.Empty<string>();
}
@@ -17,6 +18,8 @@ public sealed class SettingAttribute : Attribute
public bool IsMandatory { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public bool IsSecret { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public Type? Validator { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public Type? Converter { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
@@ -25,6 +28,6 @@ public sealed class SettingAttribute : Attribute
public override string ToString()
{
return string.Format("name = '{0}', is-mandatory = {1}, validator = {2}, converter = {3}", Name, IsMandatory ? "yes" : "no", Validator?.GetType().Name ?? "<not-set>", Converter?.GetType().Name ?? "<not-set>");
return string.Format("name = '{0}', is-mandatory = {1}, is-secret = {2}, validator = {3}, converter = {4}", Name, IsMandatory ? "yes" : "no", IsSecret ? "yes" : "no", Validator?.GetType().Name ?? "<not-set>", Converter?.GetType().Name ?? "<not-set>");
}
}

View File

@@ -100,7 +100,7 @@ internal sealed class JobExecutor
{
continue;
}
sb.AppendFormat("\t- {0}: {1}", vr.PropertyName, vr.Justification);
sb.AppendFormat("\t- {0}: {1}", vr.ValueName, vr.Justification);
sb.AppendLine();
}
throw new CertMgrException(sb.ToString());

View File

@@ -1,5 +1,6 @@
using System.Diagnostics;
using CertMgr.Core.Utils;
using CertMgr.Core.Validation;
namespace CertMgr.Core.Jobs;
@@ -8,7 +9,7 @@ public abstract class JobSettings
{
protected JobSettings()
{
ValidationResults = new ValidationResults();
ValidationResults = new ValidationResults(GetType().ToString(false));
}
public ValidationResults ValidationResults { [DebuggerStepThrough] get; }

View File

@@ -0,0 +1,146 @@
using System.Diagnostics;
using System.Reflection;
using CertMgr.Core.Attributes;
using CertMgr.Core.Converters;
using CertMgr.Core.Jobs;
using CertMgr.Core.Log;
using CertMgr.Core.Utils;
using CertMgr.Core.Validation;
namespace CertMgr.Core;
internal sealed class PropertyDescriptor
{
private readonly PropertyInfo _propertyInfo;
private readonly SettingAttribute _settingAttribute;
private readonly Type _settingsType;
internal PropertyDescriptor(PropertyInfo propertyInfo, SettingAttribute settingAttribute, Type settingsType)
{
_propertyInfo = propertyInfo;
_settingAttribute = settingAttribute;
_settingsType = settingsType;
PropertyTypeInfo = TypeUtils.UnwrapCollection(propertyInfo.PropertyType);
}
public string SettingName => _settingAttribute.Name;
public string[] SettingAlternateNames => _settingAttribute.AlternateNames;
public bool IsMandatory => _settingAttribute.IsMandatory;
public bool IsSecret => _settingAttribute.IsSecret;
public string PropertyName => _propertyInfo.Name;
public Utils.TypeInfo PropertyTypeInfo { [DebuggerStepThrough] get; }
public Type? ValidatorType => _settingAttribute.Validator;
public IValueValidator? CustomValidator
{
get
{
IValueValidator? validator = null;
if (_settingAttribute.Validator != null)
{
validator = (IValueValidator?)Activator.CreateInstance(_settingAttribute.Validator, [PropertyName]);
if (validator == null)
{
CLog.Error("Failed to create instance of value-validator of type '{0}' for property '{1}' in class '{2}'", _settingAttribute.Validator.ToString(false), PropertyName, _settingsType.ToString(false));
throw new CertMgrException("Failed to create instance of value-validator of type '{0}' for property '{1}' in class '{2}'", _settingAttribute.Validator.ToString(false), PropertyName, _settingsType.ToString(false));
}
}
return validator;
}
}
public IValueConverter? CustomConverter
{
get
{
IValueConverter? converter = null;
Type? valueConverter = _settingAttribute.Converter;
if (valueConverter != null)
{
if (typeof(IValueConverter).IsAssignableFrom(valueConverter))
{
converter = (IValueConverter?)Activator.CreateInstance(valueConverter);
}
else
{
CLog.Error("Argument '{0}' has converter specified but its type doesn't implement '{1}' and cannot be used", _settingAttribute.Name, typeof(IValueConverter));
}
}
return converter;
}
}
public void SetValue(JobSettings settings, object? value)
{
_propertyInfo.SetValue(settings, value);
}
public bool TrySetDefaultValue(JobSettings settings)
{
bool succeeded = false;
if (_settingAttribute.Default != null)
{
Utils.TypeInfo typeInfo = TypeUtils.UnwrapCollection(_propertyInfo.PropertyType);
if (_settingAttribute.Default.GetType() == typeInfo.ElementType)
{
_propertyInfo.SetValue(settings, _settingAttribute.Default);
succeeded = true;
}
else
{
CLog.Error("Default value for argument '{0}' is specified, but its type is '{1}' instead of expected '{2}'", SettingName, _settingAttribute.Default?.GetType().ToString(false) ?? "<null>", typeInfo.ElementType.ToString(false));
}
}
return succeeded;
}
/// <summary>Returns types that validator handles. Usually there is just one type, but class can inherit from multiple ISettingValidator&lt;T&gt; types.</summary>
/// <param name="validatorType">Type used to create instance of a type</param>
/// <returns>List of types handled by validator.</returns>
private static IReadOnlyList<Type> GetValidatedTypes(Type validatorType)
{
if (validatorType == null)
{
throw new ArgumentNullException(nameof(validatorType));
}
List<Type> validatedTypes = new List<Type>();
if (validatorType.IsInterface && validatorType.IsGenericType && validatorType.GetGenericTypeDefinition() == typeof(IValueValidator<>))
{
Type candidate = validatorType.GetGenericArguments()[0];
validatedTypes.Add(candidate);
}
foreach (Type iface in validatorType.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IValueValidator<>))
{
Type candidate = iface.GetGenericArguments()[0];
if (!validatedTypes.Contains(candidate))
{
validatedTypes.Add(candidate);
}
}
}
return validatedTypes;
}
}

View File

@@ -1,6 +1,5 @@
using System.Collections;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
@@ -26,6 +25,66 @@ internal sealed class SettingsBuilder
_settingsType = settingsType;
}
public async Task<JobSettings> LoadAsync(CancellationToken cancellationToken)
{
JobSettings settings = CreateSettingsInstance();
foreach (PropertyDescriptor descriptor in GetPropertiesWithSettingAttribute())
{
AsyncResult<object?> setPropertyResult = await SetPropertyValueAsync(settings, descriptor, cancellationToken).ConfigureAwait(false);
if (setPropertyResult.IsSuccess)
{
try
{
IValueValidator? validator = descriptor.CustomValidator;
if (validator != null)
{
IValidationResult valres = await validator.ValidateAsync(setPropertyResult.Value, cancellationToken).ConfigureAwait(false);
settings.ValidationResults.Add(valres);
// IReadOnlyList<Type> validatedTypes = GetValidatedTypes(descriptor.ValidatorType);
//
// if (descriptor.PropertyTypeInfo.IsCollection)
// {
// Utils.TypeInfo validatedTypeInfo = TypeUtils.UnwrapCollection(validatedTypes[0]);
// if (validatedTypeInfo.IsCollection)
// {
// // validator validates collection => send whole collection as argument to ValidateAsync(..)
// ValidationResult valres = await validator.ValidateAsync(setPropertyResult.Value, cancellationToken).ConfigureAwait(false);
// settings.ValidationResults.Add(valres);
// }
// else
// {
// // validator validates elements => send items one by one to ValidateAsync(..)
// if (setPropertyResult.Value is IList list)
// {
// foreach (object value in list)
// {
// ValidationResult valres = await validator.ValidateAsync(value, cancellationToken).ConfigureAwait(false);
// settings.ValidationResults.Add(valres);
// }
// }
// }
// }
// else
// {
// // setting is not a collection. lets assume the validator doesn't validate collection as well
// ValidationResult valres = await validator.ValidateAsync(setPropertyResult.Value, cancellationToken).ConfigureAwait(false);
// settings.ValidationResults.Add(valres);
// }
}
}
catch (Exception e)
{
CLog.Error(e, "Failed to validate property '{0}' (of type '{1}') in settings of type '{2}'", descriptor.PropertyName, descriptor.PropertyTypeInfo.SourceType.ToString(false), _settingsType.ToString(false));
throw new CertMgrException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", descriptor.PropertyName, descriptor.PropertyTypeInfo.SourceType.ToString(false), _settingsType.ToString(false));
}
}
}
return settings;
}
private static IReadOnlyList<Type> GetValidatedTypes(Type validatorType)
{
if (validatorType == null)
@@ -35,7 +94,7 @@ internal sealed class SettingsBuilder
List<Type> hits = new List<Type>();
if (validatorType.IsInterface && validatorType.IsGenericType && validatorType.GetGenericTypeDefinition() == typeof(ISettingValidator<>))
if (validatorType.IsInterface && validatorType.IsGenericType && validatorType.GetGenericTypeDefinition() == typeof(IValueValidator<>))
{
Type candidate = validatorType.GetGenericArguments()[0];
hits.Add(candidate);
@@ -43,7 +102,7 @@ internal sealed class SettingsBuilder
foreach (Type iface in validatorType.GetInterfaces())
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(ISettingValidator<>))
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IValueValidator<>))
{
Type candidate = iface.GetGenericArguments()[0];
if (!hits.Contains(candidate))
@@ -56,120 +115,52 @@ internal sealed class SettingsBuilder
return hits;
}
private async Task<AsyncResult<object?>> SetPropertyValueAsync(JobSettings settings, PropertyInfo propertyInfo, TypeInfo propertyType, SettingAttribute settingAttribute, CancellationToken cancellationToken)
private async Task<AsyncResult<object?>> SetPropertyValueAsync(JobSettings settings, PropertyDescriptor descriptor, CancellationToken cancellationToken)
{
object? convertedValue = null;
bool valueSet = false;
if (TryGetRawArgument(settingAttribute, out RawArgument? rawArg))
if (TryGetRawArgument(descriptor, out RawArgument? rawArg))
{
try
{
AsyncResult<object?> conversionResult = await ConvertRawValueAsync(settingAttribute, rawArg, propertyType.IsCollection, propertyInfo.PropertyType, propertyType.ElementType, cancellationToken).ConfigureAwait(false);
AsyncResult<object?> conversionResult = await ConvertRawValueAsync(descriptor, rawArg, cancellationToken).ConfigureAwait(false);
if (conversionResult.IsSuccess)
{
propertyInfo.SetValue(settings, conversionResult.Value);
descriptor.SetValue(settings, conversionResult.Value);
convertedValue = conversionResult.Value;
valueSet = true;
}
}
catch (Exception e)
{
CLog.Error(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
throw new CertMgrException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
CLog.Error(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", descriptor.PropertyName, descriptor.PropertyTypeInfo.SourceType.ToString(false), _settingsType.ToString(false));
throw new CertMgrException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", descriptor.PropertyName, descriptor.PropertyTypeInfo.SourceType.ToString(false), _settingsType.ToString(false));
}
}
else if (settingAttribute.IsMandatory)
else if (descriptor.IsMandatory)
{
ValidationResult valres = new ValidationResult(settingAttribute.Name, false, "Mandatory argument is missing");
ValidationResult valres = new ValidationResult(descriptor.SettingName, false, "Mandatory argument is missing");
settings.ValidationResults.Add(valres);
CLog.Error("mandatory argument '{0}' is missing", settingAttribute.Name);
}
else if (settingAttribute.Default != null)
{
TypeInfo typeInfo = UnwrapCollection(propertyInfo.PropertyType);
if (settingAttribute.Default.GetType() == typeInfo.ElementType)
{
propertyInfo.SetValue(settings, settingAttribute.Default);
CLog.Error("mandatory argument '{0}' is missing", descriptor.SettingName);
}
else
{
CLog.Error("Default value for argument '{0}' is specified, but its type is '{1}' instead of expected '{2}'", settingAttribute.Name, settingAttribute.Default?.GetType().ToString(false) ?? "<null>", typeInfo.ElementType.ToString(false));
descriptor.TrySetDefaultValue(settings);
}
}
return new AsyncResult<object?>(valueSet, convertedValue);
}
public async Task<JobSettings> LoadAsync(CancellationToken cancellationToken)
{
JobSettings settings = CreateSettingsInstance();
foreach ((PropertyInfo propertyInfo, SettingAttribute settingAttribute) in GetPropertiesWithSettingAttribute())
{
TypeInfo propertyType = UnwrapCollection(propertyInfo.PropertyType);
AsyncResult<object?> setPropertyResult = await SetPropertyValueAsync(settings, propertyInfo, propertyType, settingAttribute, cancellationToken).ConfigureAwait(false);
if (setPropertyResult.IsSuccess)
{
try
{
if (settingAttribute.Validator != null)
{
IReadOnlyList<Type> validatedTypes = GetValidatedTypes(settingAttribute.Validator);
ISettingValidator? validatorInst = (ISettingValidator?)Activator.CreateInstance(settingAttribute.Validator, [settingAttribute.Name]);
if (propertyType.IsCollection)
{
TypeInfo validatedTypeInfo = UnwrapCollection(validatedTypes[0]);
if (validatedTypeInfo.IsCollection)
{
// validator validates collection => send whole collection as argument to ValidateAsync(..)
ValidationResult valres = await validatorInst.ValidateAsync(setPropertyResult.Value, cancellationToken).ConfigureAwait(false);
settings.ValidationResults.Add(valres);
}
else
{
// validator validates elements => send items one by one to ValidateAsync(..)
if (setPropertyResult.Value is IList list)
{
foreach (object value in list)
{
ValidationResult valres = await validatorInst.ValidateAsync(value, cancellationToken).ConfigureAwait(false);
settings.ValidationResults.Add(valres);
}
}
}
}
else
{
// setting is not a collection. lets assume the validator doesn't validate collection as well
ValidationResult valres = await validatorInst.ValidateAsync(setPropertyResult.Value, cancellationToken).ConfigureAwait(false);
settings.ValidationResults.Add(valres);
}
}
}
catch (Exception e)
{
CLog.Error(e, "Failed to validate property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
throw new CertMgrException(e, "Failed to process property '{0}' (of type '{1}') in settings of type '{2}'", propertyInfo.Name, propertyInfo.PropertyType.ToString(false), _settingsType.ToString(false));
}
}
}
return settings;
}
private bool TryGetRawArgument(SettingAttribute settingAttribute, [NotNullWhen(true)] out RawArgument? rawArg)
private bool TryGetRawArgument(PropertyDescriptor descriptor, [NotNullWhen(true)] out RawArgument? rawArg)
{
rawArg = null;
if (!_rawArgs.TryGet(settingAttribute.Name, out rawArg))
if (!_rawArgs.TryGet(descriptor.SettingName, out rawArg))
{
if (settingAttribute.AlternateNames != null)
if (descriptor.SettingAlternateNames != null)
{
foreach (string altName in settingAttribute.AlternateNames)
foreach (string altName in descriptor.SettingAlternateNames)
{
if (_rawArgs.TryGet(altName, out rawArg))
{
@@ -182,7 +173,7 @@ internal sealed class SettingsBuilder
return rawArg != null;
}
private IEnumerable<(PropertyInfo, SettingAttribute)> GetPropertiesWithSettingAttribute()
private IEnumerable<PropertyDescriptor> GetPropertiesWithSettingAttribute()
{
foreach (PropertyInfo propertyInfo in _settingsType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
@@ -192,7 +183,7 @@ internal sealed class SettingsBuilder
continue;
}
yield return (propertyInfo, settingAttribute);
yield return new PropertyDescriptor(propertyInfo, settingAttribute, _settingsType);
}
}
@@ -220,21 +211,21 @@ internal sealed class SettingsBuilder
return settings;
}
private async Task<AsyncResult<object?>> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type collectionType, Type elementType, CancellationToken cancellationToken)
private async Task<AsyncResult<object?>> ConvertRawValueAsync(PropertyDescriptor descriptor, RawArgument rawArg, CancellationToken cancellationToken)
{
bool success = false;
object? convertedValue = null;
IValueConverter? customConverter = GetCustomConverter(settingAttribute);
IValueConverter? customConverter = descriptor.CustomConverter;
if (isCollection)
if (descriptor.PropertyTypeInfo.IsCollection)
{
Type listType = typeof(List<>).MakeGenericType(elementType);
Type listType = typeof(List<>).MakeGenericType(descriptor.PropertyTypeInfo.ElementType);
IList values = (IList)Activator.CreateInstance(listType)!;
foreach (string rawValue in rawArg.Values)
{
AsyncResult<object?> converted = await ConvertValueAsync(rawValue, customConverter, elementType, cancellationToken).ConfigureAwait(false);
AsyncResult<object?> converted = await ConvertValueAsync(rawValue, customConverter, descriptor.PropertyTypeInfo.ElementType, cancellationToken).ConfigureAwait(false);
if (converted.IsSuccess)
{
values.Add(converted.Value);
@@ -245,7 +236,7 @@ internal sealed class SettingsBuilder
}
else
{
AsyncResult<object?> converted = await ConvertValueAsync(rawArg.Values.First(), customConverter, elementType, cancellationToken).ConfigureAwait(false);
AsyncResult<object?> converted = await ConvertValueAsync(rawArg.Values.First(), customConverter, descriptor.PropertyTypeInfo.ElementType, cancellationToken).ConfigureAwait(false);
if (converted.IsSuccess)
{
convertedValue = converted.Value;
@@ -253,11 +244,11 @@ internal sealed class SettingsBuilder
}
else
{
CLog.Error("Cannot convert value '{0}' of argument '{1}' to type '{2}'", rawArg.Values.First(), settingAttribute.Name, elementType.ToString(false));
CLog.Error("Cannot convert value '{0}' of argument '{1}' to type '{2}'", rawArg.Values.First(), descriptor.SettingName, descriptor.PropertyTypeInfo.ElementType.ToString(false));
}
}
return new AsyncResult<object?>(success, convertedValue);
return AsyncResult<object?>.Create(success, convertedValue);
}
private async Task<AsyncResult<object?>> ConvertValueAsync(string rawValue, IValueConverter? customConverter, Type elementType, CancellationToken cancellationToken)
@@ -330,26 +321,6 @@ internal sealed class SettingsBuilder
return fallback;
}*/
private IValueConverter? GetCustomConverter(SettingAttribute settingAttribute)
{
IValueConverter? customConverter = null;
Type? valueConverter = settingAttribute.Converter;
if (valueConverter != null)
{
if (typeof(IValueConverter).IsAssignableFrom(valueConverter))
{
customConverter = (IValueConverter?)Activator.CreateInstance(valueConverter);
}
else
{
CLog.Error("Argument '{0}' has converter specified but its type doesn't implement '{1}' and cannot be used", settingAttribute.Name, typeof(IValueConverter));
}
}
return customConverter;
}
private bool TryConvertValue(string rawValue, Type targetType, out object? convertedValue)
{
convertedValue = null;
@@ -386,52 +357,4 @@ internal sealed class SettingsBuilder
return convertedValue != null;
}
private static TypeInfo UnwrapCollection(Type type)
{
if (type == typeof(string))
{
return new TypeInfo(false, type);
}
Type testedType = type;
Type? underlying = Nullable.GetUnderlyingType(type);
if (underlying != null)
{
testedType = underlying;
}
if (testedType.IsArray)
{
return new TypeInfo(true, testedType.GetElementType()!);
}
if (testedType.IsGenericType && testedType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return new TypeInfo(true, testedType.GetGenericArguments()[0]);
}
foreach (Type i in testedType.GetInterfaces())
{
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return new TypeInfo(true, i.GetGenericArguments()[0]);
}
}
return new TypeInfo(false, type);
}
private sealed class TypeInfo
{
public TypeInfo(bool isCollection, Type elementType)
{
IsCollection = isCollection;
ElementType = elementType;
}
public bool IsCollection { [DebuggerStepThrough] get; }
public Type ElementType { [DebuggerStepThrough] get; }
}
}

View File

@@ -0,0 +1,24 @@
using System.Diagnostics;
namespace CertMgr.Core.Utils;
public sealed class TypeInfo
{
public TypeInfo(Type sourceType, bool isCollection, Type elementType)
{
SourceType = sourceType;
IsCollection = isCollection;
ElementType = elementType;
}
public Type SourceType { [DebuggerStepThrough] get; }
public bool IsCollection { [DebuggerStepThrough] get; }
public Type ElementType { [DebuggerStepThrough] get; }
public override string ToString()
{
return string.Format("is-collection = {0}, source-type = '{1}', element-type = '{2}'", IsCollection ? "yes" : "no", SourceType.ToString(false), ElementType.ToString(false));
}
}

View File

@@ -0,0 +1,74 @@
namespace CertMgr.Core.Utils;
public static class TypeUtils
{
public static TypeInfo UnwrapCollection(Type type)
{
if (type == typeof(string))
{
return new TypeInfo(type, false, type);
}
Type testedType = type;
Type? underlying = Nullable.GetUnderlyingType(type);
if (underlying != null)
{
testedType = underlying;
}
if (testedType.IsArray)
{
return new TypeInfo(type, true, testedType.GetElementType()!);
}
if (testedType.IsGenericType && testedType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return new TypeInfo(type, true, testedType.GetGenericArguments()[0]);
}
foreach (Type i in testedType.GetInterfaces())
{
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return new TypeInfo(type, true, i.GetGenericArguments()[0]);
}
}
return new TypeInfo(type, false, type);
}
public static bool IsCollection(Type type)
{
if (type == typeof(string))
{
return false;
}
Type testedType = type;
Type? underlying = Nullable.GetUnderlyingType(type);
if (underlying != null)
{
testedType = underlying;
}
if (testedType.IsArray)
{
return true;
}
if (testedType.IsGenericType && testedType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return true;
}
foreach (Type i in testedType.GetInterfaces())
{
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
return true;
}
}
return false;
}
}

View File

@@ -1,10 +0,0 @@
using System.Diagnostics;
namespace CertMgr.Core.Validation;
public interface ISettingValidator
{
string SettingName { [DebuggerStepThrough] get; }
Task<ValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken);
}

View File

@@ -1,6 +0,0 @@
namespace CertMgr.Core.Validation;
public interface ISettingValidator<T> : ISettingValidator
{
Task<ValidationResult> ValidateAsync(T? settingValue, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,10 @@
namespace CertMgr.Core.Validation;
public interface IValidationResult
{
string ValueName { get; }
string Justification { get; }
bool IsValid { get; }
}

View File

@@ -0,0 +1,10 @@
using System.Diagnostics;
namespace CertMgr.Core.Validation;
public interface IValueValidator
{
string ValueName { [DebuggerStepThrough] get; }
Task<IValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,6 @@
namespace CertMgr.Core.Validation;
public interface IValueValidator<T> : IValueValidator
{
Task<IValidationResult> ValidateAsync(T? settingValue, CancellationToken cancellationToken);
}

View File

@@ -1,32 +0,0 @@
using System.Diagnostics;
using CertMgr.Core.Utils;
namespace CertMgr.Core.Validation;
public abstract class SettingValidator<T> : ISettingValidator<T>
{
protected SettingValidator(string settingName)
{
SettingName = settingName;
}
public string SettingName { [DebuggerStepThrough] get; }
public abstract Task<ValidationResult> ValidateAsync(T? settingValue, CancellationToken cancellationToken);
public Task<ValidationResult> ValidateAsync(object? settingValue, CancellationToken cancellationToken)
{
T? typedValue;
try
{
typedValue = (T?)settingValue;
}
catch (Exception e)
{
throw new CertMgrException(e, "SettingValidator of type '{0}' failed to convert value of type '{1}' to type '{2}' (setting-name = '{3}')", GetType().ToString(false), settingValue?.GetType().ToString(false) ?? "<null>", typeof(T).ToString(false), SettingName);
}
return ValidateAsync(typedValue, cancellationToken);
}
}

View File

@@ -4,22 +4,22 @@ namespace CertMgr.Core.Validation;
public sealed class StringValidator
{
public sealed class IsNotNull : ISettingValidator<string>
public sealed class IsNotNullValidator : IValueValidator<string>
{
public IsNotNull(string settingName)
public IsNotNullValidator(string valueName)
{
SettingName = settingName;
ValueName = valueName;
}
public string SettingName { [DebuggerStepThrough] get; }
public string ValueName { [DebuggerStepThrough] get; }
public Task<ValidationResult> ValidateAsync(string? value, CancellationToken cancellationToken)
public Task<IValidationResult> ValidateAsync(string? value, CancellationToken cancellationToken)
{
ValidationResult result = new ValidationResult(SettingName, value != null, "value is null");
IValidationResult result = new ValidationResult(ValueName, value != null, "value is null");
return Task.FromResult(result);
}
public Task<ValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken)
public Task<IValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken)
{
return ValidateAsync(value as string, cancellationToken);
}

View File

@@ -4,23 +4,23 @@ using CertMgr.Core.Utils;
namespace CertMgr.Core.Validation;
public sealed class ValidationResult
public class ValidationResult : IValidationResult
{
public ValidationResult(string propertyName, bool isValid, string justificationFormat, params object?[] justificationArgs)
public ValidationResult(string valueName, bool isValid, string justificationFormat, params object?[] justificationArgs)
{
PropertyName = propertyName;
ValueName = valueName;
Justification = StringFormatter.Format(justificationFormat, justificationArgs);
IsValid = isValid;
}
public string PropertyName { [DebuggerStepThrough] get; }
public string ValueName { [DebuggerStepThrough] get; }
public string Justification { [DebuggerStepThrough] get; }
public bool IsValid { [DebuggerStepThrough] get; }
public virtual bool IsValid { [DebuggerStepThrough] get; }
public override string ToString()
{
return string.Format("{0}: {1} => {2}", PropertyName, IsValid ? "valid" : "not valid", Justification);
return string.Format("{0}: is-valid = {1} => '{2}'", ValueName, IsValid ? "yes" : "no", Justification);
}
}

View File

@@ -2,40 +2,41 @@
namespace CertMgr.Core.Validation;
public sealed class ValidationResults : IReadOnlyCollection<ValidationResult>
public sealed class ValidationResults : ValidationResult, IReadOnlyCollection<IValidationResult>
{
private readonly List<ValidationResult> _results;
private readonly List<IValidationResult> _results;
internal ValidationResults()
internal ValidationResults(string valueName)
: base(valueName, true, "See subresults")
{
_results = new List<ValidationResult>();
_results = new List<IValidationResult>();
}
public int Count => _results.Count;
public bool IsValid => _results.All(res => res.IsValid);
public override bool IsValid => _results.All(res => res.IsValid);
internal void Add(ValidationResult result)
internal void Add(IValidationResult result)
{
_results.Add(result);
}
internal void Add(IReadOnlyCollection<ValidationResult> results)
internal void Add(IReadOnlyCollection<IValidationResult> results)
{
_results.AddRange(results);
}
internal void AddValid(string propertyName, string justificationFormat, params object?[] justificationArgs)
internal void AddValid(string valueName, string justificationFormat, params object?[] justificationArgs)
{
Add(new ValidationResult(propertyName, true, justificationFormat, justificationArgs));
Add(new ValidationResult(valueName, true, justificationFormat, justificationArgs));
}
internal void AddInvalid(string propertyName, string justificationFormat, params object?[] justificationArgs)
internal void AddInvalid(string valueName, string justificationFormat, params object?[] justificationArgs)
{
Add(new ValidationResult(propertyName, false, justificationFormat, justificationArgs));
Add(new ValidationResult(valueName, false, justificationFormat, justificationArgs));
}
public IEnumerator<ValidationResult> GetEnumerator()
public IEnumerator<IValidationResult> GetEnumerator()
{
return _results.GetEnumerator();
}
@@ -47,6 +48,6 @@ public sealed class ValidationResults : IReadOnlyCollection<ValidationResult>
public override string ToString()
{
return string.Format("is-valid = {0}, total-count = {1}, invalid-count = {2}", IsValid ? "yes" : "no", _results.Count, _results.Where(r => !r.IsValid).Count());
return string.Format("{0}: is-valid = {1}, total-count = {2}, invalid-count = {3}", ValueName, IsValid ? "yes" : "no", _results.Count, _results.Where(r => !r.IsValid).Count());
}
}

View File

@@ -0,0 +1,32 @@
using System.Diagnostics;
using CertMgr.Core.Utils;
namespace CertMgr.Core.Validation;
public abstract class ValueValidator<T> : IValueValidator<T>
{
protected ValueValidator(string valueName)
{
ValueName = valueName;
}
public string ValueName { [DebuggerStepThrough] get; }
public abstract Task<IValidationResult> ValidateAsync(T? value, CancellationToken cancellationToken);
public Task<IValidationResult> ValidateAsync(object? value, CancellationToken cancellationToken)
{
T? typedValue;
try
{
typedValue = (T?)value;
}
catch (Exception e)
{
throw new CertMgrException(e, "'{0}': failed to convert value of type '{1}' to type '{2}' (value-name = '{3}')", GetType().ToString(false), value?.GetType().ToString(false) ?? "<null>", typeof(T).ToString(false), ValueName);
}
return ValidateAsync(typedValue, cancellationToken);
}
}

View File

@@ -28,7 +28,7 @@ public sealed class CertificateSettings : JobSettings
// - DNS:hostname (i.e. with 'DNS:' prefix)
// - IP:ip-address (i.e. with 'IP:' prefix)
// other types (URI, UPN, email) are not supported
[Setting("subject-alternate-name", AlternateNames = ["san"], Validator = typeof(SubjectAlternateNameValidator))]
[Setting("subject-alternate-name", AlternateNames = ["san"], Validator = typeof(SubjectAlternateNamesValidator))]
public IReadOnlyCollection<string>? SubjectAlternateNames { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("algorithm", Default = CertificateAlgorithm.ECDsa, Converter = typeof(EnumConverter))]
@@ -49,13 +49,13 @@ public sealed class CertificateSettings : JobSettings
[Setting("issuer-certificate", Converter = typeof(StorageConverter))]
public IStorage? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("issuer-password")]
[Setting("issuer-password", IsSecret = true)]
public string? IssuerPassword { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("storage", IsMandatory = true, Converter = typeof(StorageConverter))]
public IStorage? Storage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("password")]
[Setting("password", IsSecret = true)]
public string? Password { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("validity-period", Default = "365d", Converter = typeof(TimeSpanConverter))]

View File

@@ -41,6 +41,7 @@ public sealed class CreateCertificateJob : Job<CertificateSettings>
}
}
return CreateSuccess("Certificate was successfully created");
}

View File

@@ -3,33 +3,33 @@ using CertMgr.Core.Validation;
namespace CertMgr.Jobs;
internal sealed class SubjectAlternateNameValidator : SettingValidator<string>
internal sealed class SubjectAlternateNameValidator : ValueValidator<string>
{
public SubjectAlternateNameValidator(string settingName)
: base(settingName)
public SubjectAlternateNameValidator(string valueName)
: base(valueName)
{
}
public override Task<ValidationResult> ValidateAsync(string? settingValue, CancellationToken cancellationToken)
public override Task<IValidationResult> ValidateAsync(string? value, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(settingValue))
if (string.IsNullOrEmpty(value))
{
return Task.FromResult(new ValidationResult(SettingName, false, "value must not be empty"));
return Task.FromResult((IValidationResult)new ValidationResult(ValueName, false, "value must not be empty"));
}
ReadOnlySpan<char> span = settingValue.AsSpan();
ReadOnlySpan<char> span = value.AsSpan();
if (span.StartsWith("DNS:", StringComparison.OrdinalIgnoreCase))
{
if (!NetUtils.IsValidDns(span.Slice(4)))
{
return Task.FromResult(new ValidationResult(SettingName, false, "value '{0}' is not valid DNS name", span.Slice(4).ToString()));
return Task.FromResult((IValidationResult)new ValidationResult(ValueName, false, "value '{0}' is not valid DNS name", span.Slice(4).ToString()));
}
}
else if (span.StartsWith("IP:", StringComparison.OrdinalIgnoreCase))
{
if (!NetUtils.IsValidIPAny(span.Slice(3)))
{
return Task.FromResult(new ValidationResult(SettingName, false, "value '{0}' is not valid IP address", span.Slice(3).ToString()));
return Task.FromResult((IValidationResult)new ValidationResult(ValueName, false, "value '{0}' is not valid IP address", span.Slice(3).ToString()));
}
}
else
@@ -37,10 +37,10 @@ internal sealed class SubjectAlternateNameValidator : SettingValidator<string>
// fallback to dns name as other alt-names (UPN, email, URI..) are not supported
if (!NetUtils.IsValidDns(span))
{
return Task.FromResult(new ValidationResult(SettingName, false, "value '{0}' is not valid DNS name (no prefix)", span.Slice(4).ToString()));
return Task.FromResult((IValidationResult)new ValidationResult(ValueName, false, "value '{0}' is not valid DNS name (no prefix)", span.ToString()));
}
}
return Task.FromResult(new ValidationResult(SettingName, true, "valid"));
return Task.FromResult((IValidationResult)new ValidationResult(ValueName, true, "valid"));
}
}

View File

@@ -0,0 +1,32 @@
using CertMgr.Core.Validation;
namespace CertMgr.Jobs;
internal sealed class SubjectAlternateNamesValidator : ValueValidator<IEnumerable<string>>
{
public SubjectAlternateNamesValidator(string valueName)
: base(valueName)
{
}
public override async Task<IValidationResult> ValidateAsync(IEnumerable<string>? values, CancellationToken cancellationToken)
{
if (values == null)
{
return new ValidationResult(ValueName, false, "collection is null");
}
ValidationResults results = new ValidationResults(ValueName);
int index = 0;
foreach (string value in values)
{
SubjectAlternateNameValidator validator = new SubjectAlternateNameValidator(ValueName + "[" + index + "]");
IValidationResult subresult = await validator.ValidateAsync(value, cancellationToken).ConfigureAwait(false);
results.Add(subresult);
index++;
}
return results;
}
}