diff --git a/.build/BuildToolkit.ps1 b/.build/BuildToolkit.ps1 index 3cfa888..20fcab0 100644 --- a/.build/BuildToolkit.ps1 +++ b/.build/BuildToolkit.ps1 @@ -1,8 +1,8 @@ # Tool Versions -$NunitVersion = "3.11.1"; +$NunitVersion = "3.12.0"; $OpenCoverVersion = "4.7.922"; $DocFxVersion = "2.56.2"; -$ReportGeneratorVersion = "4.6.7"; +$ReportGeneratorVersion = "4.8.7"; # Folder Pathes $RootPath = $MyInvocation.PSScriptRoot; diff --git a/Directory.Build.targets b/Directory.Build.targets index ed12867..0d17468 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -2,7 +2,7 @@ - 3.0.0 + 3.2.0 @@ -24,9 +24,9 @@ - - - + + + \ No newline at end of file diff --git a/Directory.build.props b/Directory.build.props index 69a3fff..b8c7941 100644 --- a/Directory.build.props +++ b/Directory.build.props @@ -1,6 +1,6 @@ - 8.0 + 9.0 diff --git a/VERSION b/VERSION index 13d683c..a0cd9f0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.1 \ No newline at end of file +3.1.0 \ No newline at end of file diff --git a/src/Moryx.ClientFramework.Kernel/Configuration/LocalConfigProvider.cs b/src/Moryx.ClientFramework.Kernel/Configuration/LocalConfigProvider.cs index bc16c94..1cc0b20 100644 --- a/src/Moryx.ClientFramework.Kernel/Configuration/LocalConfigProvider.cs +++ b/src/Moryx.ClientFramework.Kernel/Configuration/LocalConfigProvider.cs @@ -24,7 +24,7 @@ public LocalConfigProvider(IConfigManager configManager, ModulesConfiguration mo _configManager = configManager; } - /// + /// public T GetModuleConfiguration(string name) where T : class, IClientModuleConfig, new() { var config = GetConfiguration(); diff --git a/src/Moryx.ClientFramework.Kernel/Extensions/ApplicationRuntimeExtensions.cs b/src/Moryx.ClientFramework.Kernel/Extensions/ApplicationRuntimeExtensions.cs new file mode 100644 index 0000000..0306a24 --- /dev/null +++ b/src/Moryx.ClientFramework.Kernel/Extensions/ApplicationRuntimeExtensions.cs @@ -0,0 +1,18 @@ +using Moryx.Identity; + +namespace Moryx.ClientFramework.Kernel +{ + /// + /// Extensions for the + /// + public static class ApplicationRuntimeExtensions + { + /// + /// Method to register a custom ClaimsAuthorizationManager + /// + public static void EnableAuthorization(this IApplicationRuntime hol, IAuthorizationContext authorizationContext) + { + IdentityConfiguration.CurrentContext = authorizationContext; + } + } +} diff --git a/src/Moryx.ClientFramework.Kernel/HeartOfLead.cs b/src/Moryx.ClientFramework.Kernel/HeartOfLead.cs index b70d221..5da2783 100644 --- a/src/Moryx.ClientFramework.Kernel/HeartOfLead.cs +++ b/src/Moryx.ClientFramework.Kernel/HeartOfLead.cs @@ -15,6 +15,7 @@ using Caliburn.Micro; using CommandLine; using Moryx.ClientFramework.Localization; +using Moryx.Configuration; using Moryx.Container; using Moryx.Logging; using Moryx.Threading; @@ -24,7 +25,7 @@ namespace Moryx.ClientFramework.Kernel { /// - /// Main class to create ClientFramwork UI's + /// Main class to create ClientFramework UIs /// public class HeartOfLead : HeartOfLead { @@ -35,16 +36,21 @@ public HeartOfLead(string[] args) : base(args) } /// - /// Main class to create ClientFramwork UI's + /// Main class to create ClientFramework UIs /// - public class HeartOfLead : ILoggingHost + public class HeartOfLead : IApplicationRuntime, ILoggingHost where TCommandLineArguments : DefaultCommandLineArguments { #region Fields and Properties string ILoggingHost.Name => "ClientKernel"; + + /// IModuleLogger ILoggingHost.Logger { get; set; } + /// + IContainer IApplicationRuntime.GlobalContainer => _container; + /// /// Returns the current /// @@ -58,7 +64,7 @@ public class HeartOfLead : ILoggingHost /// /// Flag if the HeartOfLead is initialized /// - public bool IsInitialied { get; private set; } + public bool IsInitialied { get; private set; } // TODO: Rename to IsInitialized in the next major private GlobalContainer _container; private IKernelConfigManager _configManager; @@ -100,7 +106,7 @@ public void Initialize() if (IsInitialied) throw new InvalidOperationException("HeartOfLead is already initialized!"); - // Initialize platfrom + // Initialize platform WpfPlatform.SetProduct(); // Attach this Application to the console. @@ -331,7 +337,8 @@ private void LoadConfiguration() // Configure config manager _configManager = new KernelConfigManager { ConfigDirectory = CommandLineOptions.ConfigFolder }; - _container.SetInstance(_configManager); + _container.SetInstance(_configManager, "KernelConfigManager"); + _container.SetInstance(_configManager, "ConfigManager"); // Load global app config AppConfig = _configManager.GetConfiguration(); diff --git a/src/Moryx.ClientFramework.Kernel/RunMode/LocalRunMode.cs b/src/Moryx.ClientFramework.Kernel/RunMode/LocalRunMode.cs index 5870424..208069e 100644 --- a/src/Moryx.ClientFramework.Kernel/RunMode/LocalRunMode.cs +++ b/src/Moryx.ClientFramework.Kernel/RunMode/LocalRunMode.cs @@ -25,7 +25,7 @@ protected override Predicate TypeLoadFilter get { return type => type.GetCustomAttribute() == null; } } - /// + /// public override void LoadModulesConfiguration() { var modulesConfig = ConfigManager.GetConfiguration(); diff --git a/src/Moryx.ClientFramework.Kernel/RunMode/LocalRunModeBase.cs b/src/Moryx.ClientFramework.Kernel/RunMode/LocalRunModeBase.cs index 37e6b7a..5f8ee46 100644 --- a/src/Moryx.ClientFramework.Kernel/RunMode/LocalRunModeBase.cs +++ b/src/Moryx.ClientFramework.Kernel/RunMode/LocalRunModeBase.cs @@ -11,7 +11,7 @@ namespace Moryx.ClientFramework.Kernel { /// - /// Base class for local run modes. + /// Base class for local run modes. /// Will load and from the app domain /// public abstract class LocalRunModeBase : RunModeBase @@ -21,7 +21,7 @@ public abstract class LocalRunModeBase : RunModeBase /// /// Config manager to load kernel configurations /// - public IKernelConfigManager ConfigManager { get; set; } + public IKernelConfigManager ConfigManager { get; set; } // TODO: Change type to IConfigManager in future #endregion @@ -75,7 +75,7 @@ public override void Initialize() /// public override void LoadModulesConfiguration() { - + } } } diff --git a/src/Moryx.ClientFramework/Moryx.ClientFramework.csproj.DotSettings b/src/Moryx.ClientFramework/Moryx.ClientFramework.csproj.DotSettings index 1dc9c7a..66454ca 100644 --- a/src/Moryx.ClientFramework/Moryx.ClientFramework.csproj.DotSettings +++ b/src/Moryx.ClientFramework/Moryx.ClientFramework.csproj.DotSettings @@ -11,6 +11,7 @@ True True True + True True True True diff --git a/src/Moryx.ClientFramework/Principals/BooleanPermissionExtension.cs b/src/Moryx.ClientFramework/Principals/BooleanPermissionExtension.cs new file mode 100644 index 0000000..d5b4471 --- /dev/null +++ b/src/Moryx.ClientFramework/Principals/BooleanPermissionExtension.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2021, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using System.Windows.Markup; + +namespace Moryx.ClientFramework.Principals +{ + /// + /// Extension to determine the boolean result depends to the permission + /// + public class BooleanPermissionExtension : PermissionExtensionBase + { + /// + /// Flag to inverse the boolean result + /// + [ConstructorArgument("Inverse")] + public bool Inverse { get; set; } + + /// + protected override object ProvidePermissionBasedValue(bool hasPermission) + { + return Inverse ? !hasPermission : hasPermission; + } + } +} \ No newline at end of file diff --git a/src/Moryx.ClientFramework/Principals/ClaimsPrincipalSync.cs b/src/Moryx.ClientFramework/Principals/ClaimsPrincipalSync.cs new file mode 100644 index 0000000..acbf7c4 --- /dev/null +++ b/src/Moryx.ClientFramework/Principals/ClaimsPrincipalSync.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2021, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using System; + +namespace Moryx.ClientFramework.Principals +{ + /// + /// Helper to inform the UI about an update of the ClaimsPrincipal + /// + public static class ClaimsPrincipalSync + { + /// + /// Event to get informed about an update of the ClaimsPrincipal + /// + public static event EventHandler PrincipalChanged; + + /// + /// Method to invoke an event after an update of the ClaimsPrincipal + /// + public static void OnClaimsPrincipalChanged() + { + PrincipalChanged?.Invoke(typeof(ClaimsPrincipalSync), EventArgs.Empty); + } + } +} diff --git a/src/Moryx.ClientFramework/Principals/GridLengthPermissionExtension.cs b/src/Moryx.ClientFramework/Principals/GridLengthPermissionExtension.cs new file mode 100644 index 0000000..9b284a5 --- /dev/null +++ b/src/Moryx.ClientFramework/Principals/GridLengthPermissionExtension.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2021, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using System.Windows; + +namespace Moryx.ClientFramework.Principals +{ + /// + /// Extension to determine the length of a grid depends to the permission + /// + public class GridLengthPermissionExtension : PermissionExtensionBase + { + /// + protected override object ProvidePermissionBasedValue(bool hasPermission) + { + return hasPermission ? new GridLength(1, GridUnitType.Star) : new GridLength(0); + } + } +} \ No newline at end of file diff --git a/src/Moryx.ClientFramework/Principals/PermissionExtensionBase.cs b/src/Moryx.ClientFramework/Principals/PermissionExtensionBase.cs new file mode 100644 index 0000000..b831436 --- /dev/null +++ b/src/Moryx.ClientFramework/Principals/PermissionExtensionBase.cs @@ -0,0 +1,112 @@ +// Copyright (c) 2021, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using System; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Windows; +using System.Windows.Markup; +using System.Xaml; +using Moryx.Identity; + +namespace Moryx.ClientFramework.Principals +{ + /// + /// Base class for permission based value determination + /// + public abstract class PermissionExtensionBase : MarkupExtension + { + #region Fields and Properties + + private object _targetObject; + + private object _targetProperty; + + /// + /// Resource within the action requires permissions + /// + public string Resource { get; set; } + + /// + /// The requested action which will be validated by the current permissions + /// + [ConstructorArgument("action")] + public string Action { get; set; } + + #endregion + + /// + /// Constructor to prepare the extension to get information about changed principals + /// + protected PermissionExtensionBase() + { + ClaimsPrincipalSync.PrincipalChanged += OnPrincipalChanged; + } + + private void OnPrincipalChanged(object sender, EventArgs args) + { + if (!(_targetObject is DependencyObject targetObject)) + return; + + // Current determined value to update + var value = ProvidePermissionBasedValue(HasPermission()); + if (_targetProperty is DependencyProperty targetProperty) + { + // Update directly if can be accessed otherwise invoke the dispatcher + if (targetObject.CheckAccess()) + targetObject.SetValue(targetProperty, value); + else + targetObject.Dispatcher.Invoke(() => targetObject.SetValue(targetProperty, value)); + } + else + { + var propertyInfo = _targetProperty as PropertyInfo; + propertyInfo?.SetValue(targetObject, value, null); + } + } + + /// + public override object ProvideValue(IServiceProvider serviceProvider) + { + // If resource was not specified, tried to determine from host control + if (string.IsNullOrEmpty(Resource) && serviceProvider.GetService(typeof(IRootObjectProvider)) is IRootObjectProvider root) + { + // Try to read from root attached property + var rootElement = root.RootObject as DependencyObject; + var defaultResource = rootElement?.GetValue(PermissionProvider.DefaultResourceProperty); + if (defaultResource != null) + { + Resource = (string) defaultResource; + } + + if (string.IsNullOrEmpty(Resource)) + { + var regex = new Regex(@"^\w+\.\w+"); + Resource = regex.Match(root.RootObject?.GetType().Namespace ?? "Moryx").Value; + } + } + + if (serviceProvider.GetService(typeof(IProvideValueTarget)) is IProvideValueTarget target) + { + _targetObject = target.TargetObject; + _targetProperty = target.TargetProperty; + } + + var hasPermission = HasPermission(); + return ProvidePermissionBasedValue(hasPermission); + } + + private bool HasPermission() + { + if (IdentityConfiguration.CurrentContext != null) + return IdentityConfiguration.CurrentContext.CheckAccess(Resource, Action); + + return true; + } + + /// + /// Get the permission based value + /// + protected abstract object ProvidePermissionBasedValue(bool hasPermission); + } +} \ No newline at end of file diff --git a/src/Moryx.ClientFramework/Principals/PermissionProvider.cs b/src/Moryx.ClientFramework/Principals/PermissionProvider.cs new file mode 100644 index 0000000..13994ca --- /dev/null +++ b/src/Moryx.ClientFramework/Principals/PermissionProvider.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2021, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using System.Windows; + +namespace Moryx.ClientFramework.Principals +{ + /// + /// Class to provide attached dependency property for permission based authorization + /// + public class PermissionProvider : DependencyObject + { + /// + /// Property to handle the default resource for the + /// + public static readonly DependencyProperty DefaultResourceProperty = DependencyProperty.RegisterAttached( + "DefaultResource", typeof(string), typeof(PermissionProvider), new PropertyMetadata(default(string))); + + /// + /// Sets the default resource + /// + public static void SetDefaultResource(DependencyObject element, string value) + { + element.SetValue(DefaultResourceProperty, value); + } + + /// + /// Returns the default resource + /// + public static string GetDefaultResource(DependencyObject element) + { + return (string) element.GetValue(DefaultResourceProperty); + } + } +} \ No newline at end of file diff --git a/src/Moryx.ClientFramework/Principals/VisibilityPermissionExtension.cs b/src/Moryx.ClientFramework/Principals/VisibilityPermissionExtension.cs new file mode 100644 index 0000000..90eb856 --- /dev/null +++ b/src/Moryx.ClientFramework/Principals/VisibilityPermissionExtension.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2021, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using System.Windows; +using System.Windows.Markup; + +namespace Moryx.ClientFramework.Principals +{ + /// + /// Extension to determine the visibility depends to the permission + /// + public class VisibilityPermissionExtension : PermissionExtensionBase + { + /// + /// Flag to inverse the visibility result + /// + [ConstructorArgument("Inverse")] + public bool Inverse { get; set; } + + /// + protected override object ProvidePermissionBasedValue(bool hasPermission) + { + if (Inverse) + return hasPermission ? Visibility.Collapsed : Visibility.Visible; + + return hasPermission ? Visibility.Visible : Visibility.Collapsed; + } + } +} \ No newline at end of file diff --git a/src/Moryx.WpfToolkit/Moryx.WpfToolkit.csproj.DotSettings b/src/Moryx.WpfToolkit/Moryx.WpfToolkit.csproj.DotSettings index c3581f8..a6ba707 100644 --- a/src/Moryx.WpfToolkit/Moryx.WpfToolkit.csproj.DotSettings +++ b/src/Moryx.WpfToolkit/Moryx.WpfToolkit.csproj.DotSettings @@ -56,6 +56,7 @@ True True True + True True True True diff --git a/src/Moryx.WpfToolkit/PasswordBox/PasswordBoxHelper.cs b/src/Moryx.WpfToolkit/PasswordBox/PasswordBoxHelper.cs new file mode 100644 index 0000000..777d551 --- /dev/null +++ b/src/Moryx.WpfToolkit/PasswordBox/PasswordBoxHelper.cs @@ -0,0 +1,107 @@ +// Copyright (c) 2021, Phoenix Contact GmbH & Co. KG +// Licensed under the Apache License, Version 2.0 + +using System.Windows; +using System.Windows.Controls; + +namespace Moryx.WpfToolkit +{ + /// + /// Helper class for the password box to bind the password + /// + public static class PasswordBoxHelper + { + /// + /// Attached property for the bound password + /// + public static readonly DependencyProperty BoundPassword = + DependencyProperty.RegisterAttached("BoundPassword", typeof(string), typeof(PasswordBoxHelper), new PropertyMetadata(string.Empty, OnBoundPasswordChanged)); + + /// + /// Attached property for the bind password + /// + public static readonly DependencyProperty BindPassword = DependencyProperty.RegisterAttached( + "BindPassword", typeof(bool), typeof(PasswordBoxHelper), new PropertyMetadata(false, OnBindPasswordChanged)); + + private static readonly DependencyProperty UpdatingPassword = + DependencyProperty.RegisterAttached("UpdatingPassword", typeof(bool), typeof(PasswordBoxHelper), new PropertyMetadata(false)); + + /// + /// Gets the value + /// + public static bool GetBindPassword(DependencyObject dp) => + (bool)dp.GetValue(BindPassword); + + /// + /// Sets the value + /// + public static void SetBindPassword(DependencyObject dp, bool value) => + dp.SetValue(BindPassword, value); + + /// + /// Gets the value + /// + public static string GetBoundPassword(DependencyObject dp) => + (string)dp.GetValue(BoundPassword); + + /// + /// Sets the value + /// + public static void SetBoundPassword(DependencyObject dp, string value) => + dp.SetValue(BoundPassword, value); + + private static bool GetUpdatingPassword(DependencyObject dp) => + (bool)dp.GetValue(UpdatingPassword); + + private static void SetUpdatingPassword(DependencyObject dp, bool value) => + dp.SetValue(UpdatingPassword, value); + + private static void OnBoundPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var box = d as PasswordBox; + + // only handle this event when the property is attached to a PasswordBox + // and when the BindPassword attached property has been set to true + if (d == null || !GetBindPassword(d)) + return; + + // avoid recursive updating by ignoring the box's changed event + box.PasswordChanged -= HandlePasswordChanged; + + var newPassword = (string)e.NewValue; + + if (!GetUpdatingPassword(box)) + box.Password = newPassword; + + box.PasswordChanged += HandlePasswordChanged; + } + + private static void OnBindPasswordChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e) + { + // When the BindPassword attached property is set on a PasswordBox, + // start listening to its PasswordChanged event + if (dp is not PasswordBox box) + return; + + var wasBound = (bool)e.OldValue; + var needToBind = (bool)e.NewValue; + + if (wasBound) + box.PasswordChanged -= HandlePasswordChanged; + + if (needToBind) + box.PasswordChanged += HandlePasswordChanged; + } + + private static void HandlePasswordChanged(object sender, RoutedEventArgs e) + { + var box = sender as PasswordBox; + + // set a flag to indicate that we're updating the password + SetUpdatingPassword(box, true); + // push the new password into the BoundPassword property + SetBoundPassword(box, box.Password); + SetUpdatingPassword(box, false); + } + } +}