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 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)) if (string.IsNullOrEmpty(SubjectName))
{ {
@@ -48,7 +48,7 @@ public sealed class CertificateSettings
results.AddInvalid(nameof(ValidityPeriod), "must be greater than 1sec"); results.AddInvalid(nameof(ValidityPeriod), "must be greater than 1sec");
} }
return Task.FromResult(results); return Task.FromResult((IValidationResult)results);
} }
public override string ToString() public override string ToString()

View File

@@ -6,7 +6,7 @@ using CertMgr.Core.Validation;
namespace CertMgr.CertGen.Utils; 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. // 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) 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) 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)) 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 try
@@ -39,18 +39,18 @@ public sealed class SubjectValidator : ISettingValidator<string>
if (!EnsureAllowedAttributes(normalizedSubject, out string? error)) 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) 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); return ValidateAsync(value as string, cancellationToken);
} }
@@ -76,7 +76,7 @@ public sealed class SubjectValidator : ISettingValidator<string>
break; 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') // 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) while (currentIndex < totalLength)
{ {
@@ -161,7 +161,7 @@ public sealed class SubjectValidator : ISettingValidator<string>
continue; 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] == ',') if (currentIndex < totalLength && normalizedSubject[currentIndex] == ',')
{ {
currentIndex++; currentIndex++;
@@ -240,7 +240,7 @@ public sealed class SubjectValidator : ISettingValidator<string>
if (firstArc < 2 && secondArc > 39) 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; return false;
} }

View File

@@ -8,6 +8,7 @@ public sealed class SettingAttribute : Attribute
{ {
Name = name; Name = name;
IsMandatory = false; IsMandatory = false;
IsSecret = false;
AlternateNames = Array.Empty<string>(); AlternateNames = Array.Empty<string>();
} }
@@ -17,6 +18,8 @@ public sealed class SettingAttribute : Attribute
public bool IsMandatory { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } public bool IsMandatory { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public bool IsSecret { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public Type? Validator { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } public Type? Validator { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
public Type? Converter { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } public Type? Converter { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
@@ -25,6 +28,6 @@ public sealed class SettingAttribute : Attribute
public override string ToString() 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; continue;
} }
sb.AppendFormat("\t- {0}: {1}", vr.PropertyName, vr.Justification); sb.AppendFormat("\t- {0}: {1}", vr.ValueName, vr.Justification);
sb.AppendLine(); sb.AppendLine();
} }
throw new CertMgrException(sb.ToString()); throw new CertMgrException(sb.ToString());

View File

@@ -1,5 +1,6 @@
using System.Diagnostics; using System.Diagnostics;
using CertMgr.Core.Utils;
using CertMgr.Core.Validation; using CertMgr.Core.Validation;
namespace CertMgr.Core.Jobs; namespace CertMgr.Core.Jobs;
@@ -8,7 +9,7 @@ public abstract class JobSettings
{ {
protected JobSettings() protected JobSettings()
{ {
ValidationResults = new ValidationResults(); ValidationResults = new ValidationResults(GetType().ToString(false));
} }
public ValidationResults ValidationResults { [DebuggerStepThrough] get; } 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.Collections;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Reflection; using System.Reflection;
@@ -26,6 +25,66 @@ internal sealed class SettingsBuilder
_settingsType = settingsType; _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) private static IReadOnlyList<Type> GetValidatedTypes(Type validatorType)
{ {
if (validatorType == null) if (validatorType == null)
@@ -35,7 +94,7 @@ internal sealed class SettingsBuilder
List<Type> hits = new List<Type>(); 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]; Type candidate = validatorType.GetGenericArguments()[0];
hits.Add(candidate); hits.Add(candidate);
@@ -43,7 +102,7 @@ internal sealed class SettingsBuilder
foreach (Type iface in validatorType.GetInterfaces()) 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]; Type candidate = iface.GetGenericArguments()[0];
if (!hits.Contains(candidate)) if (!hits.Contains(candidate))
@@ -56,120 +115,52 @@ internal sealed class SettingsBuilder
return hits; 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; object? convertedValue = null;
bool valueSet = false; bool valueSet = false;
if (TryGetRawArgument(settingAttribute, out RawArgument? rawArg)) if (TryGetRawArgument(descriptor, out RawArgument? rawArg))
{ {
try 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) if (conversionResult.IsSuccess)
{ {
propertyInfo.SetValue(settings, conversionResult.Value); descriptor.SetValue(settings, conversionResult.Value);
convertedValue = conversionResult.Value; convertedValue = conversionResult.Value;
valueSet = true; valueSet = true;
} }
} }
catch (Exception e) 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)); 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}'", 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}'", 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); settings.ValidationResults.Add(valres);
CLog.Error("mandatory argument '{0}' is missing", settingAttribute.Name); CLog.Error("mandatory argument '{0}' is missing", descriptor.SettingName);
}
else if (settingAttribute.Default != null)
{
TypeInfo typeInfo = UnwrapCollection(propertyInfo.PropertyType);
if (settingAttribute.Default.GetType() == typeInfo.ElementType)
{
propertyInfo.SetValue(settings, settingAttribute.Default);
} }
else 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); return new AsyncResult<object?>(valueSet, convertedValue);
} }
public async Task<JobSettings> LoadAsync(CancellationToken cancellationToken) private bool TryGetRawArgument(PropertyDescriptor descriptor, [NotNullWhen(true)] out RawArgument? rawArg)
{
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)
{ {
rawArg = null; 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)) if (_rawArgs.TryGet(altName, out rawArg))
{ {
@@ -182,7 +173,7 @@ internal sealed class SettingsBuilder
return rawArg != null; return rawArg != null;
} }
private IEnumerable<(PropertyInfo, SettingAttribute)> GetPropertiesWithSettingAttribute() private IEnumerable<PropertyDescriptor> GetPropertiesWithSettingAttribute()
{ {
foreach (PropertyInfo propertyInfo in _settingsType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) foreach (PropertyInfo propertyInfo in _settingsType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{ {
@@ -192,7 +183,7 @@ internal sealed class SettingsBuilder
continue; continue;
} }
yield return (propertyInfo, settingAttribute); yield return new PropertyDescriptor(propertyInfo, settingAttribute, _settingsType);
} }
} }
@@ -220,21 +211,21 @@ internal sealed class SettingsBuilder
return settings; 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; bool success = false;
object? convertedValue = null; 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)!; IList values = (IList)Activator.CreateInstance(listType)!;
foreach (string rawValue in rawArg.Values) 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) if (converted.IsSuccess)
{ {
values.Add(converted.Value); values.Add(converted.Value);
@@ -245,7 +236,7 @@ internal sealed class SettingsBuilder
} }
else 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) if (converted.IsSuccess)
{ {
convertedValue = converted.Value; convertedValue = converted.Value;
@@ -253,11 +244,11 @@ internal sealed class SettingsBuilder
} }
else 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) private async Task<AsyncResult<object?>> ConvertValueAsync(string rawValue, IValueConverter? customConverter, Type elementType, CancellationToken cancellationToken)
@@ -330,26 +321,6 @@ internal sealed class SettingsBuilder
return fallback; 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) private bool TryConvertValue(string rawValue, Type targetType, out object? convertedValue)
{ {
convertedValue = null; convertedValue = null;
@@ -386,52 +357,4 @@ internal sealed class SettingsBuilder
return convertedValue != null; 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 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); 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); return ValidateAsync(value as string, cancellationToken);
} }

View File

@@ -4,23 +4,23 @@ using CertMgr.Core.Utils;
namespace CertMgr.Core.Validation; 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); Justification = StringFormatter.Format(justificationFormat, justificationArgs);
IsValid = isValid; IsValid = isValid;
} }
public string PropertyName { [DebuggerStepThrough] get; } public string ValueName { [DebuggerStepThrough] get; }
public string Justification { [DebuggerStepThrough] get; } public string Justification { [DebuggerStepThrough] get; }
public bool IsValid { [DebuggerStepThrough] get; } public virtual bool IsValid { [DebuggerStepThrough] get; }
public override string ToString() 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; 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 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); _results.Add(result);
} }
internal void Add(IReadOnlyCollection<ValidationResult> results) internal void Add(IReadOnlyCollection<IValidationResult> results)
{ {
_results.AddRange(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(); return _results.GetEnumerator();
} }
@@ -47,6 +48,6 @@ public sealed class ValidationResults : IReadOnlyCollection<ValidationResult>
public override string ToString() 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) // - DNS:hostname (i.e. with 'DNS:' prefix)
// - IP:ip-address (i.e. with 'IP:' prefix) // - IP:ip-address (i.e. with 'IP:' prefix)
// other types (URI, UPN, email) are not supported // 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; } public IReadOnlyCollection<string>? SubjectAlternateNames { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("algorithm", Default = CertificateAlgorithm.ECDsa, Converter = typeof(EnumConverter))] [Setting("algorithm", Default = CertificateAlgorithm.ECDsa, Converter = typeof(EnumConverter))]
@@ -49,13 +49,13 @@ public sealed class CertificateSettings : JobSettings
[Setting("issuer-certificate", Converter = typeof(StorageConverter))] [Setting("issuer-certificate", Converter = typeof(StorageConverter))]
public IStorage? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } public IStorage? Issuer { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("issuer-password")] [Setting("issuer-password", IsSecret = true)]
public string? IssuerPassword { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } public string? IssuerPassword { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("storage", IsMandatory = true, Converter = typeof(StorageConverter))] [Setting("storage", IsMandatory = true, Converter = typeof(StorageConverter))]
public IStorage? Storage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } public IStorage? Storage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("password")] [Setting("password", IsSecret = true)]
public string? Password { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } public string? Password { [DebuggerStepThrough] get; [DebuggerStepThrough] set; }
[Setting("validity-period", Default = "365d", Converter = typeof(TimeSpanConverter))] [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"); return CreateSuccess("Certificate was successfully created");
} }

View File

@@ -3,33 +3,33 @@ using CertMgr.Core.Validation;
namespace CertMgr.Jobs; namespace CertMgr.Jobs;
internal sealed class SubjectAlternateNameValidator : SettingValidator<string> internal sealed class SubjectAlternateNameValidator : ValueValidator<string>
{ {
public SubjectAlternateNameValidator(string settingName) public SubjectAlternateNameValidator(string valueName)
: base(settingName) : 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 (span.StartsWith("DNS:", StringComparison.OrdinalIgnoreCase))
{ {
if (!NetUtils.IsValidDns(span.Slice(4))) 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)) else if (span.StartsWith("IP:", StringComparison.OrdinalIgnoreCase))
{ {
if (!NetUtils.IsValidIPAny(span.Slice(3))) 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 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 // fallback to dns name as other alt-names (UPN, email, URI..) are not supported
if (!NetUtils.IsValidDns(span)) 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;
}
}