From fd0bdced116a4d562f9c6ed55a6e8a6087338703 Mon Sep 17 00:00:00 2001 From: grim Date: Sat, 25 Oct 2025 09:03:51 +0200 Subject: [PATCH] fixes, improvements --- certmgr/AssemblyInfo.cs | 4 + certmgr/CertGen/CertificateSettings.cs | 6 +- certmgr/CertGen/Utils/SubjectValidator.cs | 24 +- certmgr/Core/Attributes/SettingAttribute.cs | 5 +- certmgr/Core/JobExecutor.cs | 2 +- certmgr/Core/Jobs/JobSettings.cs | 3 +- certmgr/Core/PropertyDescriptor.cs | 146 ++++++++++ certmgr/Core/SettingsBuilder.cs | 253 ++++++------------ certmgr/Core/Utils/TypeInfo.cs | 24 ++ certmgr/Core/Utils/TypeUtils.cs | 74 +++++ certmgr/Core/Validation/ISettingValidator.cs | 10 - certmgr/Core/Validation/ISettingValidatorT.cs | 6 - certmgr/Core/Validation/IValidationResult.cs | 10 + certmgr/Core/Validation/IValueValidator.cs | 10 + certmgr/Core/Validation/IValueValidatorT.cs | 6 + certmgr/Core/Validation/SettingValidatorT.cs | 32 --- certmgr/Core/Validation/StringValidator.cs | 14 +- certmgr/Core/Validation/ValidationResult.cs | 12 +- certmgr/Core/Validation/ValidationResults.cs | 27 +- certmgr/Core/Validation/ValueValidatorT.cs | 32 +++ certmgr/Jobs/CertificateSettings.cs | 6 +- certmgr/Jobs/CreateCertificateJob.cs | 1 + certmgr/Jobs/SubjectAlternateNameValidator.cs | 22 +- .../Jobs/SubjectAlternateNamesValidator.cs | 32 +++ 24 files changed, 490 insertions(+), 271 deletions(-) create mode 100644 certmgr/AssemblyInfo.cs create mode 100644 certmgr/Core/PropertyDescriptor.cs create mode 100644 certmgr/Core/Utils/TypeInfo.cs create mode 100644 certmgr/Core/Utils/TypeUtils.cs delete mode 100644 certmgr/Core/Validation/ISettingValidator.cs delete mode 100644 certmgr/Core/Validation/ISettingValidatorT.cs create mode 100644 certmgr/Core/Validation/IValidationResult.cs create mode 100644 certmgr/Core/Validation/IValueValidator.cs create mode 100644 certmgr/Core/Validation/IValueValidatorT.cs delete mode 100644 certmgr/Core/Validation/SettingValidatorT.cs create mode 100644 certmgr/Core/Validation/ValueValidatorT.cs create mode 100644 certmgr/Jobs/SubjectAlternateNamesValidator.cs diff --git a/certmgr/AssemblyInfo.cs b/certmgr/AssemblyInfo.cs new file mode 100644 index 0000000..f17d624 --- /dev/null +++ b/certmgr/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("certmgrTest")] \ No newline at end of file diff --git a/certmgr/CertGen/CertificateSettings.cs b/certmgr/CertGen/CertificateSettings.cs index f798fa8..0037189 100644 --- a/certmgr/CertGen/CertificateSettings.cs +++ b/certmgr/CertGen/CertificateSettings.cs @@ -34,9 +34,9 @@ public sealed class CertificateSettings public X509KeyUsageFlags KeyUsage { [DebuggerStepThrough] get; [DebuggerStepThrough] set; } - public Task ValidateAsync(CancellationToken cancellationToken) + public Task 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() diff --git a/certmgr/CertGen/Utils/SubjectValidator.cs b/certmgr/CertGen/Utils/SubjectValidator.cs index 0736843..ad35d20 100644 --- a/certmgr/CertGen/Utils/SubjectValidator.cs +++ b/certmgr/CertGen/Utils/SubjectValidator.cs @@ -6,7 +6,7 @@ using CertMgr.Core.Validation; namespace CertMgr.CertGen.Utils; -public sealed class SubjectValidator : ISettingValidator +public sealed class SubjectValidator : IValueValidator { // the list contains most used attributes, but more of them exists. Extend the list if needed. private static readonly HashSet AllowedAttributes = new HashSet(StringComparer.OrdinalIgnoreCase) @@ -19,16 +19,16 @@ public sealed class SubjectValidator : ISettingValidator public SubjectValidator(string settingName) { - SettingName = settingName; + ValueName = settingName; } - public string SettingName { [DebuggerStepThrough] get; } + public string ValueName { [DebuggerStepThrough] get; } - public Task ValidateAsync(string? settingValue, CancellationToken cancellationToken) + public Task 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 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 ?? "", e.GetType().Name, e.Message)); + return Task.FromResult((IValidationResult)new ValidationResult(ValueName, false, "invalid value: '{0}'. Exception = {1}: {2}", settingValue ?? "", e.GetType().Name, e.Message)); } } - public Task ValidateAsync(object? value, CancellationToken cancellationToken) + public Task ValidateAsync(object? value, CancellationToken cancellationToken) { return ValidateAsync(value as string, cancellationToken); } @@ -76,7 +76,7 @@ public sealed class SubjectValidator : ISettingValidator 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 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 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; } diff --git a/certmgr/Core/Attributes/SettingAttribute.cs b/certmgr/Core/Attributes/SettingAttribute.cs index 96b489d..70ab4b1 100644 --- a/certmgr/Core/Attributes/SettingAttribute.cs +++ b/certmgr/Core/Attributes/SettingAttribute.cs @@ -8,6 +8,7 @@ public sealed class SettingAttribute : Attribute { Name = name; IsMandatory = false; + IsSecret = false; AlternateNames = Array.Empty(); } @@ -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 ?? "", Converter?.GetType().Name ?? ""); + 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 ?? "", Converter?.GetType().Name ?? ""); } } diff --git a/certmgr/Core/JobExecutor.cs b/certmgr/Core/JobExecutor.cs index 293e0f5..6b0c214 100644 --- a/certmgr/Core/JobExecutor.cs +++ b/certmgr/Core/JobExecutor.cs @@ -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()); diff --git a/certmgr/Core/Jobs/JobSettings.cs b/certmgr/Core/Jobs/JobSettings.cs index f11cbe1..2124810 100644 --- a/certmgr/Core/Jobs/JobSettings.cs +++ b/certmgr/Core/Jobs/JobSettings.cs @@ -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; } diff --git a/certmgr/Core/PropertyDescriptor.cs b/certmgr/Core/PropertyDescriptor.cs new file mode 100644 index 0000000..e1014d8 --- /dev/null +++ b/certmgr/Core/PropertyDescriptor.cs @@ -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) ?? "", typeInfo.ElementType.ToString(false)); + } + + } + + return succeeded; + } + + /// Returns types that validator handles. Usually there is just one type, but class can inherit from multiple ISettingValidator<T> types. + /// Type used to create instance of a type + /// List of types handled by validator. + private static IReadOnlyList GetValidatedTypes(Type validatorType) + { + if (validatorType == null) + { + throw new ArgumentNullException(nameof(validatorType)); + } + + List validatedTypes = new List(); + + 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; + } +} diff --git a/certmgr/Core/SettingsBuilder.cs b/certmgr/Core/SettingsBuilder.cs index d2b312a..36c5825 100644 --- a/certmgr/Core/SettingsBuilder.cs +++ b/certmgr/Core/SettingsBuilder.cs @@ -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 LoadAsync(CancellationToken cancellationToken) + { + JobSettings settings = CreateSettingsInstance(); + + foreach (PropertyDescriptor descriptor in GetPropertiesWithSettingAttribute()) + { + AsyncResult 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 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 GetValidatedTypes(Type validatorType) { if (validatorType == null) @@ -35,7 +94,7 @@ internal sealed class SettingsBuilder List hits = new List(); - 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> SetPropertyValueAsync(JobSettings settings, PropertyInfo propertyInfo, TypeInfo propertyType, SettingAttribute settingAttribute, CancellationToken cancellationToken) + private async Task> 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 conversionResult = await ConvertRawValueAsync(settingAttribute, rawArg, propertyType.IsCollection, propertyInfo.PropertyType, propertyType.ElementType, cancellationToken).ConfigureAwait(false); + AsyncResult 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); + CLog.Error("mandatory argument '{0}' is missing", descriptor.SettingName); } - else if (settingAttribute.Default != null) + else { - TypeInfo typeInfo = UnwrapCollection(propertyInfo.PropertyType); - if (settingAttribute.Default.GetType() == typeInfo.ElementType) - { - propertyInfo.SetValue(settings, settingAttribute.Default); - } - 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) ?? "", typeInfo.ElementType.ToString(false)); - } - + descriptor.TrySetDefaultValue(settings); } + return new AsyncResult(valueSet, convertedValue); } - public async Task LoadAsync(CancellationToken cancellationToken) - { - JobSettings settings = CreateSettingsInstance(); - - foreach ((PropertyInfo propertyInfo, SettingAttribute settingAttribute) in GetPropertiesWithSettingAttribute()) - { - TypeInfo propertyType = UnwrapCollection(propertyInfo.PropertyType); - - AsyncResult setPropertyResult = await SetPropertyValueAsync(settings, propertyInfo, propertyType, settingAttribute, cancellationToken).ConfigureAwait(false); - if (setPropertyResult.IsSuccess) - { - try - { - if (settingAttribute.Validator != null) - { - IReadOnlyList 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 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> ConvertRawValueAsync(SettingAttribute settingAttribute, RawArgument rawArg, bool isCollection, Type collectionType, Type elementType, CancellationToken cancellationToken) + private async Task> 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 converted = await ConvertValueAsync(rawValue, customConverter, elementType, cancellationToken).ConfigureAwait(false); + AsyncResult 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 converted = await ConvertValueAsync(rawArg.Values.First(), customConverter, elementType, cancellationToken).ConfigureAwait(false); + AsyncResult 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(success, convertedValue); + return AsyncResult.Create(success, convertedValue); } private async Task> 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; } - } } diff --git a/certmgr/Core/Utils/TypeInfo.cs b/certmgr/Core/Utils/TypeInfo.cs new file mode 100644 index 0000000..3f145ab --- /dev/null +++ b/certmgr/Core/Utils/TypeInfo.cs @@ -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)); + } +} diff --git a/certmgr/Core/Utils/TypeUtils.cs b/certmgr/Core/Utils/TypeUtils.cs new file mode 100644 index 0000000..a11dd5a --- /dev/null +++ b/certmgr/Core/Utils/TypeUtils.cs @@ -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; + } +} diff --git a/certmgr/Core/Validation/ISettingValidator.cs b/certmgr/Core/Validation/ISettingValidator.cs deleted file mode 100644 index 23a8888..0000000 --- a/certmgr/Core/Validation/ISettingValidator.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Diagnostics; - -namespace CertMgr.Core.Validation; - -public interface ISettingValidator -{ - string SettingName { [DebuggerStepThrough] get; } - - Task ValidateAsync(object? value, CancellationToken cancellationToken); -} diff --git a/certmgr/Core/Validation/ISettingValidatorT.cs b/certmgr/Core/Validation/ISettingValidatorT.cs deleted file mode 100644 index d69668a..0000000 --- a/certmgr/Core/Validation/ISettingValidatorT.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CertMgr.Core.Validation; - -public interface ISettingValidator : ISettingValidator -{ - Task ValidateAsync(T? settingValue, CancellationToken cancellationToken); -} diff --git a/certmgr/Core/Validation/IValidationResult.cs b/certmgr/Core/Validation/IValidationResult.cs new file mode 100644 index 0000000..b3ff530 --- /dev/null +++ b/certmgr/Core/Validation/IValidationResult.cs @@ -0,0 +1,10 @@ +namespace CertMgr.Core.Validation; + +public interface IValidationResult +{ + string ValueName { get; } + + string Justification { get; } + + bool IsValid { get; } +} diff --git a/certmgr/Core/Validation/IValueValidator.cs b/certmgr/Core/Validation/IValueValidator.cs new file mode 100644 index 0000000..e616420 --- /dev/null +++ b/certmgr/Core/Validation/IValueValidator.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; + +namespace CertMgr.Core.Validation; + +public interface IValueValidator +{ + string ValueName { [DebuggerStepThrough] get; } + + Task ValidateAsync(object? value, CancellationToken cancellationToken); +} diff --git a/certmgr/Core/Validation/IValueValidatorT.cs b/certmgr/Core/Validation/IValueValidatorT.cs new file mode 100644 index 0000000..88a2208 --- /dev/null +++ b/certmgr/Core/Validation/IValueValidatorT.cs @@ -0,0 +1,6 @@ +namespace CertMgr.Core.Validation; + +public interface IValueValidator : IValueValidator +{ + Task ValidateAsync(T? settingValue, CancellationToken cancellationToken); +} diff --git a/certmgr/Core/Validation/SettingValidatorT.cs b/certmgr/Core/Validation/SettingValidatorT.cs deleted file mode 100644 index 9289401..0000000 --- a/certmgr/Core/Validation/SettingValidatorT.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Diagnostics; - -using CertMgr.Core.Utils; - -namespace CertMgr.Core.Validation; - -public abstract class SettingValidator : ISettingValidator -{ - protected SettingValidator(string settingName) - { - SettingName = settingName; - } - - public string SettingName { [DebuggerStepThrough] get; } - - public abstract Task ValidateAsync(T? settingValue, CancellationToken cancellationToken); - - public Task 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) ?? "", typeof(T).ToString(false), SettingName); - } - - return ValidateAsync(typedValue, cancellationToken); - } -} diff --git a/certmgr/Core/Validation/StringValidator.cs b/certmgr/Core/Validation/StringValidator.cs index a77e89b..806072d 100644 --- a/certmgr/Core/Validation/StringValidator.cs +++ b/certmgr/Core/Validation/StringValidator.cs @@ -4,22 +4,22 @@ namespace CertMgr.Core.Validation; public sealed class StringValidator { - public sealed class IsNotNull : ISettingValidator + public sealed class IsNotNullValidator : IValueValidator { - public IsNotNull(string settingName) + public IsNotNullValidator(string valueName) { - SettingName = settingName; + ValueName = valueName; } - public string SettingName { [DebuggerStepThrough] get; } + public string ValueName { [DebuggerStepThrough] get; } - public Task ValidateAsync(string? value, CancellationToken cancellationToken) + public Task 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 ValidateAsync(object? value, CancellationToken cancellationToken) + public Task ValidateAsync(object? value, CancellationToken cancellationToken) { return ValidateAsync(value as string, cancellationToken); } diff --git a/certmgr/Core/Validation/ValidationResult.cs b/certmgr/Core/Validation/ValidationResult.cs index 0b1e311..a106490 100644 --- a/certmgr/Core/Validation/ValidationResult.cs +++ b/certmgr/Core/Validation/ValidationResult.cs @@ -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); } } diff --git a/certmgr/Core/Validation/ValidationResults.cs b/certmgr/Core/Validation/ValidationResults.cs index c3df78c..cb5733c 100644 --- a/certmgr/Core/Validation/ValidationResults.cs +++ b/certmgr/Core/Validation/ValidationResults.cs @@ -2,40 +2,41 @@ namespace CertMgr.Core.Validation; -public sealed class ValidationResults : IReadOnlyCollection +public sealed class ValidationResults : ValidationResult, IReadOnlyCollection { - private readonly List _results; + private readonly List _results; - internal ValidationResults() + internal ValidationResults(string valueName) + : base(valueName, true, "See subresults") { - _results = new List(); + _results = new List(); } 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 results) + internal void Add(IReadOnlyCollection 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 GetEnumerator() + public IEnumerator GetEnumerator() { return _results.GetEnumerator(); } @@ -47,6 +48,6 @@ public sealed class ValidationResults : IReadOnlyCollection 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()); } } diff --git a/certmgr/Core/Validation/ValueValidatorT.cs b/certmgr/Core/Validation/ValueValidatorT.cs new file mode 100644 index 0000000..857b7e4 --- /dev/null +++ b/certmgr/Core/Validation/ValueValidatorT.cs @@ -0,0 +1,32 @@ +using System.Diagnostics; + +using CertMgr.Core.Utils; + +namespace CertMgr.Core.Validation; + +public abstract class ValueValidator : IValueValidator +{ + protected ValueValidator(string valueName) + { + ValueName = valueName; + } + + public string ValueName { [DebuggerStepThrough] get; } + + public abstract Task ValidateAsync(T? value, CancellationToken cancellationToken); + + public Task 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) ?? "", typeof(T).ToString(false), ValueName); + } + + return ValidateAsync(typedValue, cancellationToken); + } +} diff --git a/certmgr/Jobs/CertificateSettings.cs b/certmgr/Jobs/CertificateSettings.cs index 5287bc0..212bec0 100644 --- a/certmgr/Jobs/CertificateSettings.cs +++ b/certmgr/Jobs/CertificateSettings.cs @@ -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? 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))] diff --git a/certmgr/Jobs/CreateCertificateJob.cs b/certmgr/Jobs/CreateCertificateJob.cs index 0e2a618..9fc941e 100644 --- a/certmgr/Jobs/CreateCertificateJob.cs +++ b/certmgr/Jobs/CreateCertificateJob.cs @@ -41,6 +41,7 @@ public sealed class CreateCertificateJob : Job } } + return CreateSuccess("Certificate was successfully created"); } diff --git a/certmgr/Jobs/SubjectAlternateNameValidator.cs b/certmgr/Jobs/SubjectAlternateNameValidator.cs index 0cbd9c3..47fa5f9 100644 --- a/certmgr/Jobs/SubjectAlternateNameValidator.cs +++ b/certmgr/Jobs/SubjectAlternateNameValidator.cs @@ -3,33 +3,33 @@ using CertMgr.Core.Validation; namespace CertMgr.Jobs; -internal sealed class SubjectAlternateNameValidator : SettingValidator +internal sealed class SubjectAlternateNameValidator : ValueValidator { - public SubjectAlternateNameValidator(string settingName) - : base(settingName) + public SubjectAlternateNameValidator(string valueName) + : base(valueName) { } - public override Task ValidateAsync(string? settingValue, CancellationToken cancellationToken) + public override Task 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 span = settingValue.AsSpan(); + ReadOnlySpan 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 // 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")); } } diff --git a/certmgr/Jobs/SubjectAlternateNamesValidator.cs b/certmgr/Jobs/SubjectAlternateNamesValidator.cs new file mode 100644 index 0000000..e7b6567 --- /dev/null +++ b/certmgr/Jobs/SubjectAlternateNamesValidator.cs @@ -0,0 +1,32 @@ +using CertMgr.Core.Validation; + +namespace CertMgr.Jobs; + +internal sealed class SubjectAlternateNamesValidator : ValueValidator> +{ + public SubjectAlternateNamesValidator(string valueName) + : base(valueName) + { + } + + public override async Task ValidateAsync(IEnumerable? 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; + } +}