From 8efa0f96148d79a08ab04ac3a5eb35b06e077b6d Mon Sep 17 00:00:00 2001 From: Yeming Liu <11371776+isra-fel@users.noreply.github.com> Date: Thu, 24 Mar 2022 16:38:40 +0800 Subject: [PATCH 1/9] Modified code from Microsot.Extensions.Configuration --- .../Config/Internal/BinderOptions.cs | 28 + .../Config/Internal/ConfigurationBinder.cs | 587 ++++++++++++++++++ .../Config/Internal/ConfigurationBuilder.cs | 72 +++ .../Internal/ConfigurationExtensions.cs | 97 +++ .../Internal/ConfigurationKeyComparer.cs | 82 +++ .../Config/Internal/ConfigurationPath.cs | 90 +++ .../Config/Internal/ConfigurationProvider.cs | 97 +++ .../Config/Internal/ConfigurationRoot.cs | 137 ++++ .../Config/Internal/ConfigurationSection.cs | 127 ++++ .../Internal/Interfaces/IConfiguration.cs | 45 ++ .../Interfaces/IConfigurationBuilder.cs | 49 ++ .../Interfaces/IConfigurationProvider.cs | 56 ++ .../Internal/Interfaces/IConfigurationRoot.cs | 38 ++ .../Interfaces/IConfigurationSection.cs | 40 ++ .../Interfaces/IConfigurationSource.cs | 29 + .../IEnvironmentVariableProvider.cs | 28 + .../InternalConfigurationRootExtensions.cs | 42 ++ ...VariablesConfigurationBuilderExtensions.cs | 29 + ...nvironmentVariablesConfigurationOptions.cs | 27 + ...vironmentVariablesConfigurationProvider.cs | 49 ++ ...EnvironmentVariablesConfigurationSource.cs | 33 + .../Providers/JsonConfigurationExtensions.cs | 42 ++ .../Providers/JsonConfigurationFileParser.cs | 116 ++++ .../JsonStreamConfigurationProvider.cs | 37 ++ .../JsonStreamConfigurationSource.cs | 32 + .../Providers/StreamConfigurationProvider.cs | 60 ++ .../Providers/StreamConfigurationSource.cs | 37 ++ ...bleMemoryConfigurationBuilderExtensions.cs | 33 + .../UnsettableMemoryConfigurationProvider.cs | 81 +++ .../UnsettableMemoryConfigurationSource.cs | 26 + 30 files changed, 2246 insertions(+) create mode 100644 src/Accounts/Authentication/Config/Internal/BinderOptions.cs create mode 100644 src/Accounts/Authentication/Config/Internal/ConfigurationBinder.cs create mode 100644 src/Accounts/Authentication/Config/Internal/ConfigurationBuilder.cs create mode 100644 src/Accounts/Authentication/Config/Internal/ConfigurationExtensions.cs create mode 100644 src/Accounts/Authentication/Config/Internal/ConfigurationKeyComparer.cs create mode 100644 src/Accounts/Authentication/Config/Internal/ConfigurationPath.cs create mode 100644 src/Accounts/Authentication/Config/Internal/ConfigurationProvider.cs create mode 100644 src/Accounts/Authentication/Config/Internal/ConfigurationRoot.cs create mode 100644 src/Accounts/Authentication/Config/Internal/ConfigurationSection.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Interfaces/IConfiguration.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationBuilder.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationProvider.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationRoot.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationSection.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationSource.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Interfaces/IEnvironmentVariableProvider.cs create mode 100644 src/Accounts/Authentication/Config/Internal/InternalConfigurationRootExtensions.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationBuilderExtensions.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationOptions.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationProvider.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationSource.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/JsonConfigurationExtensions.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/JsonConfigurationFileParser.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/JsonStreamConfigurationProvider.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/JsonStreamConfigurationSource.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/StreamConfigurationProvider.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/StreamConfigurationSource.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/UnsettableMemoryConfigurationBuilderExtensions.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/UnsettableMemoryConfigurationProvider.cs create mode 100644 src/Accounts/Authentication/Config/Internal/Providers/UnsettableMemoryConfigurationSource.cs diff --git a/src/Accounts/Authentication/Config/Internal/BinderOptions.cs b/src/Accounts/Authentication/Config/Internal/BinderOptions.cs new file mode 100644 index 000000000000..48ada32aa867 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/BinderOptions.cs @@ -0,0 +1,28 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal +{ + /// + /// Options class used by the . + /// + internal class BinderOptions + { + /// + /// When false (the default), the binder will only attempt to set public properties. + /// If true, the binder will attempt to set all non read-only properties. + /// + public bool BindNonPublicProperties { get; set; } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/ConfigurationBinder.cs b/src/Accounts/Authentication/Config/Internal/ConfigurationBinder.cs new file mode 100644 index 000000000000..785c7a23f60d --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/ConfigurationBinder.cs @@ -0,0 +1,587 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal +{ + /// + /// Static helper class that allows binding strongly typed objects to configuration values. + /// + internal static class ConfigurationBinder + { + /// + /// Attempts to bind the configuration instance to a new instance of type T. + /// If this configuration section has a value, that will be used. + /// Otherwise binding by matching property names against configuration keys recursively. + /// + /// The type of the new instance to bind. + /// The configuration instance to bind. + /// The new instance of T if successful, default(T) otherwise. + public static (T, string) Get(this IConfiguration configuration) + => configuration.Get(_ => { }); + + /// + /// Attempts to bind the configuration instance to a new instance of type T. + /// If this configuration section has a value, that will be used. + /// Otherwise binding by matching property names against configuration keys recursively. + /// + /// The type of the new instance to bind. + /// The configuration instance to bind. + /// Configures the binder options. + /// The new instance of T if successful, default(T) otherwise. + public static (T, string) Get(this IConfiguration configuration, Action configureOptions) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + (object result, string providerId) = configuration.Get(typeof(T), configureOptions); + if (result == null) + { + return (default(T), providerId); + } + return ((T)result, providerId); + } + + /// + /// Attempts to bind the configuration instance to a new instance of type T. + /// If this configuration section has a value, that will be used. + /// Otherwise binding by matching property names against configuration keys recursively. + /// + /// The configuration instance to bind. + /// The type of the new instance to bind. + /// The new instance if successful, null otherwise. + public static (object, string) Get(this IConfiguration configuration, Type type) + => configuration.Get(type, _ => { }); + + /// + /// Attempts to bind the configuration instance to a new instance of type T. + /// If this configuration section has a value, that will be used. + /// Otherwise binding by matching property names against configuration keys recursively. + /// + /// The configuration instance to bind. + /// The type of the new instance to bind. + /// Configures the binder options. + /// The new instance if successful, null otherwise. + public static (object, string) Get(this IConfiguration configuration, Type type, Action configureOptions) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + var options = new BinderOptions(); + configureOptions?.Invoke(options); + object bound = BindInstance(type, instance: null, config: configuration, options: options); + string providerId = (configuration as IConfigurationSection).GetValueWithProviderId().Item2; + return (bound, providerId); + } + + /// + /// Attempts to bind the given object instance to the configuration section specified by the key by matching property names against configuration keys recursively. + /// + /// The configuration instance to bind. + /// The key of the configuration section to bind. + /// The object to bind. + public static void Bind(this IConfiguration configuration, string key, object instance) + => configuration.GetSection(key).Bind(instance); + + /// + /// Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively. + /// + /// The configuration instance to bind. + /// The object to bind. + public static void Bind(this IConfiguration configuration, object instance) + => configuration.Bind(instance, o => { }); + + /// + /// Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively. + /// + /// The configuration instance to bind. + /// The object to bind. + /// Configures the binder options. + public static void Bind(this IConfiguration configuration, object instance, Action configureOptions) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + if (instance != null) + { + var options = new BinderOptions(); + configureOptions?.Invoke(options); + BindInstance(instance.GetType(), instance, configuration, options); + } + } + + /// + /// Extracts the value with the specified key and converts it to type T. + /// + /// The type to convert the value to. + /// The configuration. + /// The key of the configuration section's value to convert. + /// The converted value. + public static T GetValue(this IConfiguration configuration, string key) + { + return GetValue(configuration, key, default(T)); + } + + /// + /// Extracts the value with the specified key and converts it to type T. + /// + /// The type to convert the value to. + /// The configuration. + /// The key of the configuration section's value to convert. + /// The default value to use if no value is found. + /// The converted value. + public static T GetValue(this IConfiguration configuration, string key, T defaultValue) + { + return (T)GetValue(configuration, typeof(T), key, defaultValue); + } + + /// + /// Extracts the value with the specified key and converts it to the specified type. + /// + /// The configuration. + /// The type to convert the value to. + /// The key of the configuration section's value to convert. + /// The converted value. + public static object GetValue(this IConfiguration configuration, Type type, string key) + { + return GetValue(configuration, type, key, defaultValue: null); + } + + /// + /// Extracts the value with the specified key and converts it to the specified type. + /// + /// The configuration. + /// The type to convert the value to. + /// The key of the configuration section's value to convert. + /// The default value to use if no value is found. + /// The converted value. + public static object GetValue(this IConfiguration configuration, Type type, string key, object defaultValue) + { + IConfigurationSection section = configuration.GetSection(key); + string value = section.Value; + if (value != null) + { + return ConvertValue(type, value, section.Path); + } + return defaultValue; + } + + private static void BindNonScalar(this IConfiguration configuration, object instance, BinderOptions options) + { + if (instance != null) + { + foreach (PropertyInfo property in GetAllProperties(instance.GetType().GetTypeInfo())) + { + BindProperty(property, instance, configuration, options); + } + } + } + + private static void BindProperty(PropertyInfo property, object instance, IConfiguration config, BinderOptions options) + { + // We don't support set only, non public, or indexer properties + if (property.GetMethod == null || + (!options.BindNonPublicProperties && !property.GetMethod.IsPublic) || + property.GetMethod.GetParameters().Length > 0) + { + return; + } + + object propertyValue = property.GetValue(instance); + bool hasSetter = property.SetMethod != null && (property.SetMethod.IsPublic || options.BindNonPublicProperties); + + if (propertyValue == null && !hasSetter) + { + // Property doesn't have a value and we cannot set it so there is no + // point in going further down the graph + return; + } + + propertyValue = BindInstance(property.PropertyType, propertyValue, config.GetSection(property.Name), options); + + if (propertyValue != null && hasSetter) + { + property.SetValue(instance, propertyValue); + } + } + + private static object BindToCollection(TypeInfo typeInfo, IConfiguration config, BinderOptions options) + { + Type type = typeof(List<>).MakeGenericType(typeInfo.GenericTypeArguments[0]); + object instance = Activator.CreateInstance(type); + BindCollection(instance, type, config, options); + return instance; + } + + // Try to create an array/dictionary instance to back various collection interfaces + private static object AttemptBindToCollectionInterfaces(Type type, IConfiguration config, BinderOptions options) + { + TypeInfo typeInfo = type.GetTypeInfo(); + + if (!typeInfo.IsInterface) + { + return null; + } + + Type collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyList<>), type); + if (collectionInterface != null) + { + // IEnumerable is guaranteed to have exactly one parameter + return BindToCollection(typeInfo, config, options); + } + + collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyDictionary<,>), type); + if (collectionInterface != null) + { + Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(typeInfo.GenericTypeArguments[0], typeInfo.GenericTypeArguments[1]); + object instance = Activator.CreateInstance(dictionaryType); + BindDictionary(instance, dictionaryType, config, options); + return instance; + } + + collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); + if (collectionInterface != null) + { + object instance = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(typeInfo.GenericTypeArguments[0], typeInfo.GenericTypeArguments[1])); + BindDictionary(instance, collectionInterface, config, options); + return instance; + } + + collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyCollection<>), type); + if (collectionInterface != null) + { + // IReadOnlyCollection is guaranteed to have exactly one parameter + return BindToCollection(typeInfo, config, options); + } + + collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); + if (collectionInterface != null) + { + // ICollection is guaranteed to have exactly one parameter + return BindToCollection(typeInfo, config, options); + } + + collectionInterface = FindOpenGenericInterface(typeof(IEnumerable<>), type); + if (collectionInterface != null) + { + // IEnumerable is guaranteed to have exactly one parameter + return BindToCollection(typeInfo, config, options); + } + + return null; + } + + private static object BindInstance(Type type, object instance, IConfiguration config, BinderOptions options) + { + // if binding IConfigurationSection, break early + if (type == typeof(IConfigurationSection)) + { + return config; + } + + var section = config as IConfigurationSection; + string configValue = section?.Value; + object convertedValue; + Exception error; + if (configValue != null && TryConvertValue(type, configValue, section.Path, out convertedValue, out error)) + { + if (error != null) + { + throw error; + } + + // Leaf nodes are always reinitialized + return convertedValue; + } + + if (config != null && config.GetChildren().Any()) + { + // If we don't have an instance, try to create one + if (instance == null) + { + // We are already done if binding to a new collection instance worked + instance = AttemptBindToCollectionInterfaces(type, config, options); + if (instance != null) + { + return instance; + } + + instance = CreateInstance(type); + } + + // See if its a Dictionary + Type collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); + if (collectionInterface != null) + { + BindDictionary(instance, collectionInterface, config, options); + } + else if (type.IsArray) + { + instance = BindArray((Array)instance, config, options); + } + else + { + // See if its an ICollection + collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); + if (collectionInterface != null) + { + BindCollection(instance, collectionInterface, config, options); + } + // Something else + else + { + BindNonScalar(config, instance, options); + } + } + } + + return instance; + } + + private static object CreateInstance(Type type) + { + TypeInfo typeInfo = type.GetTypeInfo(); + + if (typeInfo.IsInterface || typeInfo.IsAbstract) + { + throw new InvalidOperationException($"Error: cannot activate abstract class or interface, type: {type}"); + } + + if (type.IsArray) + { + if (typeInfo.GetArrayRank() > 1) + { + throw new InvalidOperationException($"Error: multi-dimensional array is not supported, type: {type})"); + } + + return Array.CreateInstance(typeInfo.GetElementType(), 0); + } + + if (!typeInfo.IsValueType) + { + bool hasDefaultConstructor = typeInfo.DeclaredConstructors.Any(ctor => ctor.IsPublic && ctor.GetParameters().Length == 0); + if (!hasDefaultConstructor) + { + throw new InvalidOperationException($"Error: missing parameterless constructor in type {type}"); + } + } + + try + { + return Activator.CreateInstance(type); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error: failed to activate type [{type}]. {ex.Message}", ex); + } + } + + private static void BindDictionary(object dictionary, Type dictionaryType, IConfiguration config, BinderOptions options) + { + TypeInfo typeInfo = dictionaryType.GetTypeInfo(); + + // IDictionary is guaranteed to have exactly two parameters + Type keyType = typeInfo.GenericTypeArguments[0]; + Type valueType = typeInfo.GenericTypeArguments[1]; + bool keyTypeIsEnum = keyType.GetTypeInfo().IsEnum; + + if (keyType != typeof(string) && !keyTypeIsEnum) + { + // We only support string and enum keys + return; + } + + PropertyInfo setter = typeInfo.GetDeclaredProperty("Item"); + foreach (IConfigurationSection child in config.GetChildren()) + { + object item = BindInstance( + type: valueType, + instance: null, + config: child, + options: options); + if (item != null) + { + if (keyType == typeof(string)) + { + string key = child.Key; + setter.SetValue(dictionary, item, new object[] { key }); + } + else if (keyTypeIsEnum) + { + object key = Enum.Parse(keyType, child.Key); + setter.SetValue(dictionary, item, new object[] { key }); + } + } + } + } + + private static void BindCollection(object collection, Type collectionType, IConfiguration config, BinderOptions options) + { + TypeInfo typeInfo = collectionType.GetTypeInfo(); + + // ICollection is guaranteed to have exactly one parameter + Type itemType = typeInfo.GenericTypeArguments[0]; + MethodInfo addMethod = typeInfo.GetDeclaredMethod("Add"); + + foreach (IConfigurationSection section in config.GetChildren()) + { + try + { + object item = BindInstance( + type: itemType, + instance: null, + config: section, + options: options); + if (item != null) + { + addMethod.Invoke(collection, new[] { item }); + } + } + catch + { + } + } + } + + private static Array BindArray(Array source, IConfiguration config, BinderOptions options) + { + IConfigurationSection[] children = config.GetChildren().ToArray(); + int arrayLength = source.Length; + Type elementType = source.GetType().GetElementType(); + var newArray = Array.CreateInstance(elementType, arrayLength + children.Length); + + // binding to array has to preserve already initialized arrays with values + if (arrayLength > 0) + { + Array.Copy(source, newArray, arrayLength); + } + + for (int i = 0; i < children.Length; i++) + { + try + { + object item = BindInstance( + type: elementType, + instance: null, + config: children[i], + options: options); + if (item != null) + { + newArray.SetValue(item, arrayLength + i); + } + } + catch + { + } + } + + return newArray; + } + + private static bool TryConvertValue(Type type, string value, string path, out object result, out Exception error) + { + error = null; + result = null; + if (type == typeof(object)) + { + result = value; + return true; + } + + if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + if (string.IsNullOrEmpty(value)) + { + return true; + } + return TryConvertValue(Nullable.GetUnderlyingType(type), value, path, out result, out error); + } + + TypeConverter converter = TypeDescriptor.GetConverter(type); + if (converter.CanConvertFrom(typeof(string))) + { + try + { + result = converter.ConvertFromInvariantString(value); + } + catch (Exception ex) + { + error = new InvalidOperationException($"Failed to convert value [{value}] to type [{type}].", ex); + } + return true; + } + + return false; + } + + private static object ConvertValue(Type type, string value, string path) + { + object result; + Exception error; + TryConvertValue(type, value, path, out result, out error); + if (error != null) + { + throw error; + } + return result; + } + + private static Type FindOpenGenericInterface(Type expected, Type actual) + { + TypeInfo actualTypeInfo = actual.GetTypeInfo(); + if (actualTypeInfo.IsGenericType && + actual.GetGenericTypeDefinition() == expected) + { + return actual; + } + + IEnumerable interfaces = actualTypeInfo.ImplementedInterfaces; + foreach (Type interfaceType in interfaces) + { + if (interfaceType.GetTypeInfo().IsGenericType && + interfaceType.GetGenericTypeDefinition() == expected) + { + return interfaceType; + } + } + return null; + } + + private static IEnumerable GetAllProperties(TypeInfo type) + { + var allProperties = new List(); + + do + { + allProperties.AddRange(type.DeclaredProperties); + type = type.BaseType.GetTypeInfo(); + } + while (type != typeof(object).GetTypeInfo()); + + return allProperties; + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/ConfigurationBuilder.cs b/src/Accounts/Authentication/Config/Internal/ConfigurationBuilder.cs new file mode 100644 index 000000000000..03ab860ccbe9 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/ConfigurationBuilder.cs @@ -0,0 +1,72 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System; +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal +{ + /// + /// Used to build key/value based configuration settings for use in an application. + /// + internal class ConfigurationBuilder : IConfigurationBuilder + { + /// + /// Returns the sources used to obtain configuration values. + /// + public IList Sources { get; } = new List(); + + private IDictionary _ids = new Dictionary(); + + /// + /// Gets a key/value collection that can be used to share data between the + /// and the registered s. + /// + public IDictionary Properties { get; } = new Dictionary(); + + /// + /// Adds a new configuration source. + /// + /// The configuration source to add. + /// The same . + public IConfigurationBuilder Add(string id, IConfigurationSource source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + Sources.Add(source); + _ids[source] = id; + return this; + } + + /// + /// Builds an with keys and values from the set of providers registered in + /// . + /// + /// An with keys and values from the registered providers. + public IConfigurationRoot Build() + { + var providers = new List(); + foreach (IConfigurationSource source in Sources) + { + IConfigurationProvider provider = source.Build(this, _ids[source]); + providers.Add(provider); + } + return new ConfigurationRoot(providers); + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/ConfigurationExtensions.cs b/src/Accounts/Authentication/Config/Internal/ConfigurationExtensions.cs new file mode 100644 index 000000000000..54209d3526cf --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/ConfigurationExtensions.cs @@ -0,0 +1,97 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal +{ + /// + /// Extension methods for configuration classes./>. + /// + internal static class ConfigurationExtensions + { + /// + /// Adds a new configuration source. + /// + /// The to add to. + /// Configures the source secrets. + /// The . + public static IConfigurationBuilder Add(this IConfigurationBuilder builder, string id, Action configureSource) where TSource : IConfigurationSource, new() + { + var source = new TSource(); + configureSource?.Invoke(source); + return builder.Add(id, source); + } + + /// + /// Shorthand for GetSection("ConnectionStrings")[name]. + /// + /// The configuration. + /// The connection string key. + /// The connection string. + public static string GetConnectionString(this IConfiguration configuration, string name) + { + return configuration?.GetSection("ConnectionStrings")?[name]; + } + + /// + /// Get the enumeration of key value pairs within the + /// + /// The to enumerate. + /// An enumeration of key value pairs. + public static IEnumerable> AsEnumerable(this IConfiguration configuration) => configuration.AsEnumerable(makePathsRelative: false); + + /// + /// Get the enumeration of key value pairs within the + /// + /// The to enumerate. + /// If true, the child keys returned will have the current configuration's Path trimmed from the front. + /// An enumeration of key value pairs. + public static IEnumerable> AsEnumerable(this IConfiguration configuration, bool makePathsRelative) + { + var stack = new Stack(); + stack.Push(configuration); + var rootSection = configuration as IConfigurationSection; + int prefixLength = (makePathsRelative && rootSection != null) ? rootSection.Path.Length + 1 : 0; + while (stack.Count > 0) + { + IConfiguration config = stack.Pop(); + // Don't include the sections value if we are removing paths, since it will be an empty key + if (config is IConfigurationSection section && (!makePathsRelative || config != configuration)) + { + yield return new KeyValuePair(section.Path.Substring(prefixLength), section.Value); + } + foreach (IConfigurationSection child in config.GetChildren()) + { + stack.Push(child); + } + } + } + + /// + /// Determines whether the section has a or has children + /// + public static bool Exists(this IConfigurationSection section) + { + if (section == null) + { + return false; + } + return section.Value != null || section.GetChildren().Any(); + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/ConfigurationKeyComparer.cs b/src/Accounts/Authentication/Config/Internal/ConfigurationKeyComparer.cs new file mode 100644 index 000000000000..7d55a591f2c6 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/ConfigurationKeyComparer.cs @@ -0,0 +1,82 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal +{ + internal class ConfigurationKeyComparer : IComparer + { + private static readonly string[] _keyDelimiterArray = new[] { ConfigurationPath.KeyDelimiter }; + + /// + /// The default instance. + /// + public static ConfigurationKeyComparer Instance { get; } = new ConfigurationKeyComparer(); + + /// + /// Compares two strings. + /// + /// First string. + /// Second string. + /// Less than 0 if x is less than y, 0 if x is equal to y and greater than 0 if x is greater than y. + public int Compare(string x, string y) + { + string[] xParts = x?.Split(_keyDelimiterArray, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); + string[] yParts = y?.Split(_keyDelimiterArray, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); + + // Compare each part until we get two parts that are not equal + for (int i = 0; i < Math.Min(xParts.Length, yParts.Length); i++) + { + x = xParts[i]; + y = yParts[i]; + + int value1 = 0; + int value2 = 0; + + bool xIsInt = x != null && int.TryParse(x, out value1); + bool yIsInt = y != null && int.TryParse(y, out value2); + + int result; + + if (!xIsInt && !yIsInt) + { + // Both are strings + result = string.Compare(x, y, StringComparison.OrdinalIgnoreCase); + } + else if (xIsInt && yIsInt) + { + // Both are int + result = value1 - value2; + } + else + { + // Only one of them is int + result = xIsInt ? -1 : 1; + } + + if (result != 0) + { + // One of them is different + return result; + } + } + + // If we get here, the common parts are equal. + // If they are of the same length, then they are totally identical + return xParts.Length - yParts.Length; + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/ConfigurationPath.cs b/src/Accounts/Authentication/Config/Internal/ConfigurationPath.cs new file mode 100644 index 000000000000..7a335573d7dd --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/ConfigurationPath.cs @@ -0,0 +1,90 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal +{ + /// + /// Utility methods and constants for manipulating Configuration paths + /// + internal static class ConfigurationPath + { + /// + /// The delimiter ":" used to separate individual keys in a path. + /// + public static readonly string KeyDelimiter = ":"; + + /// + /// Combines path segments into one path. + /// + /// The path segments to combine. + /// The combined path. + public static string Combine(params string[] pathSegments) + { + if (pathSegments == null) + { + throw new ArgumentNullException(nameof(pathSegments)); + } + return string.Join(KeyDelimiter, pathSegments); + } + + /// + /// Combines path segments into one path. + /// + /// The path segments to combine. + /// The combined path. + public static string Combine(IEnumerable pathSegments) + { + if (pathSegments == null) + { + throw new ArgumentNullException(nameof(pathSegments)); + } + return string.Join(KeyDelimiter, pathSegments); + } + + /// + /// Extracts the last path segment from the path. + /// + /// The path. + /// The last path segment of the path. + public static string GetSectionKey(string path) + { + if (string.IsNullOrEmpty(path)) + { + return path; + } + + int lastDelimiterIndex = path.LastIndexOf(KeyDelimiter, StringComparison.OrdinalIgnoreCase); + return lastDelimiterIndex == -1 ? path : path.Substring(lastDelimiterIndex + 1); + } + + /// + /// Extracts the path corresponding to the parent node for a given path. + /// + /// The path. + /// The original path minus the last individual segment found in it. Null if the original path corresponds to a top level node. + public static string GetParentPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + return null; + } + + int lastDelimiterIndex = path.LastIndexOf(KeyDelimiter, StringComparison.OrdinalIgnoreCase); + return lastDelimiterIndex == -1 ? null : path.Substring(0, lastDelimiterIndex); + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/ConfigurationProvider.cs b/src/Accounts/Authentication/Config/Internal/ConfigurationProvider.cs new file mode 100644 index 000000000000..f2a5c5c7a89e --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/ConfigurationProvider.cs @@ -0,0 +1,97 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal +{ + /// + /// Base helper class for implementing an + /// + internal abstract class ConfigurationProvider : IConfigurationProvider + { + public string Id { get; } + + /// + /// Initializes a new + /// + protected ConfigurationProvider(string id) + { + Id = id; + Data = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// The configuration key value pairs for this provider. + /// + protected IDictionary Data { get; set; } + + /// + /// Attempts to find a value with the given key, returns true if one is found, false otherwise. + /// + /// The key to lookup. + /// The value found at key if one is found. + /// True if key has a value, false otherwise. + public virtual bool TryGet(string key, out string value) + => Data.TryGetValue(key, out value); + + /// + /// Sets a value for a given key. + /// + /// The configuration key to set. + /// The value to set. + public virtual void Set(string key, string value) + => Data[key] = value; + + /// + /// Loads (or reloads) the data for this provider. + /// + public virtual void Load() + { } + + /// + /// Returns the list of keys that this provider has. + /// + /// The earlier keys that other providers contain. + /// The path for the parent IConfiguration. + /// The list of keys for this provider. + public virtual IEnumerable GetChildKeys( + IEnumerable earlierKeys, + string parentPath) + { + string prefix = parentPath == null ? string.Empty : parentPath + ConfigurationPath.KeyDelimiter; + + return Data + .Where(kv => kv.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + .Select(kv => Segment(kv.Key, prefix.Length)) + .Concat(earlierKeys) + .OrderBy(k => k, ConfigurationKeyComparer.Instance); + } + + private static string Segment(string key, int prefixLength) + { + int indexOf = key.IndexOf(ConfigurationPath.KeyDelimiter, prefixLength, StringComparison.OrdinalIgnoreCase); + return indexOf < 0 ? key.Substring(prefixLength) : key.Substring(prefixLength, indexOf - prefixLength); + } + + /// + /// Generates a string representing this provider name and relevant details. + /// + /// The configuration name. + public override string ToString() => $"{GetType().Name}"; + } +} diff --git a/src/Accounts/Authentication/Config/Internal/ConfigurationRoot.cs b/src/Accounts/Authentication/Config/Internal/ConfigurationRoot.cs new file mode 100644 index 000000000000..af8cd9516bee --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/ConfigurationRoot.cs @@ -0,0 +1,137 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal +{ + /// + /// The root node for a configuration. + /// + internal class ConfigurationRoot : IConfigurationRoot, IDisposable + { + private readonly IList _providers; + + /// + /// Initializes a Configuration root with a list of providers. + /// + /// The s for this configuration. + public ConfigurationRoot(IList providers) + { + if (providers == null) + { + throw new ArgumentNullException(nameof(providers)); + } + + _providers = providers; + foreach (IConfigurationProvider p in providers) + { + p.Load(); + } + } + + /// + /// The s for this configuration. + /// + public IEnumerable Providers => _providers; + + /// + /// Gets or sets the value corresponding to a configuration key. + /// + /// The configuration key. + /// The configuration value. + public string this[string key] + { + get + { + return GetValueWithProviderId(key).Item1; + } + set + { + if (!_providers.Any()) + { + throw new InvalidOperationException($"Error: none config source is registered."); + } + + foreach (IConfigurationProvider provider in _providers) + { + provider.Set(key, value); + } + } + } + + public (string, string) GetValueWithProviderId(string key) + { + for (int i = _providers.Count - 1; i >= 0; i--) + { + IConfigurationProvider provider = _providers[i]; + + if (provider.TryGet(key, out string value)) + { + return (value, provider.Id); + } + } + + return (null, null); + + } + + /// + /// Gets the immediate children sub-sections. + /// + /// The children. + public IEnumerable GetChildren() => this.GetChildrenImplementation(null); + + /// + /// Gets a configuration sub-section with the specified key. + /// + /// The key of the configuration section. + /// The . + /// + /// This method will never return null. If no matching sub-section is found with the specified key, + /// an empty will be returned. + /// + public IConfigurationSection GetSection(string key) + => new ConfigurationSection(this, key); + + /// + /// Force the configuration values to be reloaded from the underlying sources. + /// + public void Reload() + { + foreach (IConfigurationProvider provider in _providers) + { + provider.Load(); + } + } + + /// + public void Dispose() + { + // dispose providers + foreach (IConfigurationProvider provider in _providers) + { + (provider as IDisposable)?.Dispose(); + } + } + + public IConfigurationProvider GetConfigurationProvider(string id) + { + return _providers.FirstOrDefault(x => x.Id.Equals(id)); + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/ConfigurationSection.cs b/src/Accounts/Authentication/Config/Internal/ConfigurationSection.cs new file mode 100644 index 000000000000..045f31b468ad --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/ConfigurationSection.cs @@ -0,0 +1,127 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System; +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal +{ + /// + /// Represents a section of application configuration values. + /// + internal class ConfigurationSection : IConfigurationSection + { + private readonly IConfigurationRoot _root; + private readonly string _path; + private string _key; + + /// + /// Initializes a new instance. + /// + /// The configuration root. + /// The path to this section. + public ConfigurationSection(IConfigurationRoot root, string path) + { + if (root == null) + { + throw new ArgumentNullException(nameof(root)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + _root = root; + _path = path; + } + + /// + /// Gets the full path to this section from the . + /// + public string Path => _path; + + /// + /// Gets the key this section occupies in its parent. + /// + public string Key + { + get + { + if (_key == null) + { + // Key is calculated lazily as last portion of Path + _key = ConfigurationPath.GetSectionKey(_path); + } + return _key; + } + } + + /// + /// Gets or sets the section value. + /// + public string Value + { + get + { + return _root[Path]; + } + set + { + _root[Path] = value; + } + } + + public (string, string) GetValueWithProviderId() + { + return _root.GetValueWithProviderId(Path); + } + + /// + /// Gets or sets the value corresponding to a configuration key. + /// + /// The configuration key. + /// The configuration value. + public string this[string key] + { + get + { + return _root[ConfigurationPath.Combine(Path, key)]; + } + + set + { + _root[ConfigurationPath.Combine(Path, key)] = value; + } + } + + /// + /// Gets a configuration sub-section with the specified key. + /// + /// The key of the configuration section. + /// The . + /// + /// This method will never return null. If no matching sub-section is found with the specified key, + /// an empty will be returned. + /// + public IConfigurationSection GetSection(string key) => _root.GetSection(ConfigurationPath.Combine(Path, key)); + + /// + /// Gets the immediate descendant configuration sub-sections. + /// + /// The configuration sub-sections. + public IEnumerable GetChildren() => _root.GetChildrenImplementation(Path); + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Interfaces/IConfiguration.cs b/src/Accounts/Authentication/Config/Internal/Interfaces/IConfiguration.cs new file mode 100644 index 000000000000..56821ed998ef --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Interfaces/IConfiguration.cs @@ -0,0 +1,45 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces +{ + internal interface IConfiguration + { + /// + /// Gets or sets a configuration value. + /// + /// The configuration key. + /// The configuration value. + string this[string key] { get; set; } + + /// + /// Gets a configuration sub-section with the specified key. + /// + /// The key of the configuration section. + /// The . + /// + /// This method will never return null. If no matching sub-section is found with the specified key, + /// an empty will be returned. + /// + IConfigurationSection GetSection(string key); + + /// + /// Gets the immediate descendant configuration sub-sections. + /// + /// The configuration sub-sections. + IEnumerable GetChildren(); + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationBuilder.cs b/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationBuilder.cs new file mode 100644 index 000000000000..760ab3f997e7 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationBuilder.cs @@ -0,0 +1,49 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces +{ + /// + /// Represents a type used to build application configuration. + /// + internal interface IConfigurationBuilder + { + /// + /// Gets a key/value collection that can be used to share data between the + /// and the registered s. + /// + IDictionary Properties { get; } + + /// + /// Gets the sources used to obtain configuration values + /// + IList Sources { get; } + + /// + /// Adds a new configuration source. + /// + /// The configuration source to add. + /// The same . + IConfigurationBuilder Add(string id, IConfigurationSource source); + + /// + /// Builds an with keys and values from the set of sources registered in + /// . + /// + /// An with keys and values from the registered sources. + IConfigurationRoot Build(); + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationProvider.cs b/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationProvider.cs new file mode 100644 index 000000000000..235cd11c74d4 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationProvider.cs @@ -0,0 +1,56 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces +{ + /// + /// Provides configuration key/values for an application. + /// + internal interface IConfigurationProvider + { + string Id { get; } + + /// + /// Tries to get a configuration value for the specified key. + /// + /// The key. + /// The value. + /// True if a value for the specified key was found, otherwise false. + bool TryGet(string key, out string value); + + /// + /// Sets a configuration value for the specified key. + /// + /// The key. + /// The value. + void Set(string key, string value); + + /// + /// Loads configuration values from the source represented by this . + /// + void Load(); + + /// + /// Returns the immediate descendant configuration keys for a given parent path based on this + /// s data and the set of keys returned by all the preceding + /// s. + /// + /// The child keys returned by the preceding providers for the same parent path. + /// The parent path. + /// The child keys. + IEnumerable GetChildKeys(IEnumerable earlierKeys, string parentPath); + } +} \ No newline at end of file diff --git a/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationRoot.cs b/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationRoot.cs new file mode 100644 index 000000000000..e6506849bb42 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationRoot.cs @@ -0,0 +1,38 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces +{ + /// + /// Represents the root of an hierarchy. + /// + internal interface IConfigurationRoot : IConfiguration + { + /// + /// Force the configuration values to be reloaded from the underlying s. + /// + void Reload(); + + /// + /// The s for this configuration. + /// + IEnumerable Providers { get; } + + IConfigurationProvider GetConfigurationProvider(string id); + + (string, string) GetValueWithProviderId(string key); + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationSection.cs b/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationSection.cs new file mode 100644 index 000000000000..d829afa70178 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationSection.cs @@ -0,0 +1,40 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces +{ + internal interface IConfigurationSection : IConfiguration + { + /// + /// Gets the key this section occupies in its parent. + /// + string Key { get; } + + /// + /// Gets the full path to this section within the . + /// + string Path { get; } + + /// + /// Gets or sets the section value. + /// + string Value { get; set; } + + /// + /// Gets the section value and the ID of the provider which provides this value. + /// + /// + (string, string) GetValueWithProviderId(); + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationSource.cs b/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationSource.cs new file mode 100644 index 000000000000..975c175086a1 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Interfaces/IConfigurationSource.cs @@ -0,0 +1,29 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces +{ + /// + /// Represents a source of configuration key/values for an application. + /// + internal interface IConfigurationSource + { + /// + /// Builds the for this source. + /// + /// The . + /// An + IConfigurationProvider Build(IConfigurationBuilder builder, string id); + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Interfaces/IEnvironmentVariableProvider.cs b/src/Accounts/Authentication/Config/Internal/Interfaces/IEnvironmentVariableProvider.cs new file mode 100644 index 000000000000..2cf3787f0c63 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Interfaces/IEnvironmentVariableProvider.cs @@ -0,0 +1,28 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces +{ + /// + /// An abstraction of the ability to get and set environment variable on various targets. + /// + internal interface IEnvironmentVariableProvider + { + string Get(string variableName, EnvironmentVariableTarget target = EnvironmentVariableTarget.Process); + + void Set(string variableName, string value, EnvironmentVariableTarget target = EnvironmentVariableTarget.Process); + } +} diff --git a/src/Accounts/Authentication/Config/Internal/InternalConfigurationRootExtensions.cs b/src/Accounts/Authentication/Config/Internal/InternalConfigurationRootExtensions.cs new file mode 100644 index 000000000000..dcefe827f1b5 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/InternalConfigurationRootExtensions.cs @@ -0,0 +1,42 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal +{ + /// + /// Extensions method for + /// + internal static class InternalConfigurationRootExtensions + { + /// + /// Gets the immediate children sub-sections of configuration root based on key. + /// + /// Configuration from which to retrieve sub-sections. + /// Key of a section of which children to retrieve. + /// Immediate children sub-sections of section specified by key. + internal static IEnumerable GetChildrenImplementation(this IConfigurationRoot root, string path) + { + return root.Providers + .Aggregate(Enumerable.Empty(), + (seed, source) => source.GetChildKeys(seed, path)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key))); + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationBuilderExtensions.cs b/src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationBuilderExtensions.cs new file mode 100644 index 000000000000..352d8d3c08f1 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationBuilderExtensions.cs @@ -0,0 +1,29 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + internal static class EnvironmentVariablesConfigurationBuilderExtensions + { + public static IConfigurationBuilder AddEnvironmentVariables(this IConfigurationBuilder builder, + string providerId, + EnvironmentVariablesConfigurationOptions options) + { + builder.Add(providerId, new EnvironmentVariablesConfigurationSource(options)); + return builder; + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationOptions.cs b/src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationOptions.cs new file mode 100644 index 000000000000..47b90bb444fe --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationOptions.cs @@ -0,0 +1,27 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System; +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + internal class EnvironmentVariablesConfigurationOptions + { + public IEnvironmentVariableProvider EnvironmentVariableProvider { get; set; } + public EnvironmentVariableTarget EnvironmentVariableTarget { get; set; } + public IDictionary EnvironmentVariableToKeyMap { get; set; } + } +} \ No newline at end of file diff --git a/src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationProvider.cs b/src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationProvider.cs new file mode 100644 index 000000000000..ef0fcf488680 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationProvider.cs @@ -0,0 +1,49 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System; +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + internal class EnvironmentVariablesConfigurationProvider : ConfigurationProvider + { + private EnvironmentVariableTarget _environmentVariableTarget; + private IDictionary _environmentVariableNameToKeyMapping; + private IEnvironmentVariableProvider _environmentVariableProvider; + + public EnvironmentVariablesConfigurationProvider(string id, EnvironmentVariablesConfigurationOptions options) : base(id) + { + _environmentVariableTarget = options.EnvironmentVariableTarget; + _environmentVariableNameToKeyMapping = options.EnvironmentVariableToKeyMap ?? new Dictionary(); + _environmentVariableProvider = options.EnvironmentVariableProvider; + } + + public override void Load() + { + var data = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var i in _environmentVariableNameToKeyMapping) + { + string value = _environmentVariableProvider.Get(i.Key, _environmentVariableTarget); + if (!string.IsNullOrEmpty(value)) + { + data[i.Value] = value; + } + } + + Data = data; + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationSource.cs b/src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationSource.cs new file mode 100644 index 000000000000..f5b59aeebcf7 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/EnvironmentVariablesConfigurationSource.cs @@ -0,0 +1,33 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + internal class EnvironmentVariablesConfigurationSource : IConfigurationSource + { + private EnvironmentVariablesConfigurationOptions _options; + + public EnvironmentVariablesConfigurationSource(EnvironmentVariablesConfigurationOptions options) + { + _options = options; + } + + public IConfigurationProvider Build(IConfigurationBuilder builder, string id) + { + return new EnvironmentVariablesConfigurationProvider(id, _options); + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Providers/JsonConfigurationExtensions.cs b/src/Accounts/Authentication/Config/Internal/Providers/JsonConfigurationExtensions.cs new file mode 100644 index 000000000000..81850eb386d8 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/JsonConfigurationExtensions.cs @@ -0,0 +1,42 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System; +using System.IO; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + /// + /// Extension methods for adding the . + /// + internal static class JsonConfigurationExtensions + { + /// + /// Adds a JSON configuration source to . + /// + /// The to add to. + /// The to read the json configuration data from. + /// The . + public static IConfigurationBuilder AddJsonStream(this IConfigurationBuilder builder, string id, Stream stream) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.Add(id, s => s.Stream = stream); + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Providers/JsonConfigurationFileParser.cs b/src/Accounts/Authentication/Config/Internal/Providers/JsonConfigurationFileParser.cs new file mode 100644 index 000000000000..b370ec2bd9fe --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/JsonConfigurationFileParser.cs @@ -0,0 +1,116 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + internal class JsonConfigurationFileParser + { + private JsonConfigurationFileParser() { } + + private readonly IDictionary _data = new SortedDictionary(StringComparer.OrdinalIgnoreCase); + private readonly Stack _context = new Stack(); + private string _currentPath; + + public static IDictionary Parse(Stream input) + => new JsonConfigurationFileParser().ParseStream(input); + + private IDictionary ParseStream(Stream input) + { + _data.Clear(); + + var jsonDocumentOptions = new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + }; + + using (var reader = new StreamReader(input)) + using (JsonDocument doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions)) + { + if (doc.RootElement.ValueKind != JsonValueKind.Object) + { + throw new FormatException($"Error: unsupported JSON token [{doc.RootElement.ValueKind}]. Object kind is expected."); + } + VisitElement(doc.RootElement); + } + + return _data; + } + + private void VisitElement(JsonElement element) + { + foreach (JsonProperty property in element.EnumerateObject()) + { + EnterContext(property.Name); + VisitValue(property.Value); + ExitContext(); + } + } + + private void VisitValue(JsonElement value) + { + switch (value.ValueKind) + { + case JsonValueKind.Object: + VisitElement(value); + break; + + case JsonValueKind.Array: + int index = 0; + foreach (JsonElement arrayElement in value.EnumerateArray()) + { + EnterContext(index.ToString()); + VisitValue(arrayElement); + ExitContext(); + index++; + } + break; + + case JsonValueKind.Number: + case JsonValueKind.String: + case JsonValueKind.True: + case JsonValueKind.False: + case JsonValueKind.Null: + string key = _currentPath; + if (_data.ContainsKey(key)) + { + throw new FormatException($"Error: key [{key}] is duplicated."); + } + _data[key] = value.ToString(); + break; + + default: + throw new FormatException($"Error: unsupported JSON token [{value.ValueKind}]"); + } + } + + private void EnterContext(string context) + { + _context.Push(context); + _currentPath = ConfigurationPath.Combine(_context.Reverse()); + } + + private void ExitContext() + { + _context.Pop(); + _currentPath = ConfigurationPath.Combine(_context.Reverse()); + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Providers/JsonStreamConfigurationProvider.cs b/src/Accounts/Authentication/Config/Internal/Providers/JsonStreamConfigurationProvider.cs new file mode 100644 index 000000000000..c7d0297bb676 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/JsonStreamConfigurationProvider.cs @@ -0,0 +1,37 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.IO; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + /// + /// Loads configuration key/values from a json stream into a provider. + /// + internal class JsonStreamConfigurationProvider : StreamConfigurationProvider + { + public JsonStreamConfigurationProvider(JsonStreamConfigurationSource source, string id) : base(source, id) + { + } + + /// + /// Loads json configuration key/values from a stream into a provider. + /// + /// The json to load configuration data from. + public override void Load(Stream stream) + { + Data = JsonConfigurationFileParser.Parse(stream); + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Providers/JsonStreamConfigurationSource.cs b/src/Accounts/Authentication/Config/Internal/Providers/JsonStreamConfigurationSource.cs new file mode 100644 index 000000000000..8e46465f1e84 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/JsonStreamConfigurationSource.cs @@ -0,0 +1,32 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + /// + /// Represents a JSON file as an . + /// + internal class JsonStreamConfigurationSource : StreamConfigurationSource + { + /// + /// Builds the for this source. + /// + /// The . + /// An + public override IConfigurationProvider Build(IConfigurationBuilder builder, string id) + => new JsonStreamConfigurationProvider(this, id); + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Providers/StreamConfigurationProvider.cs b/src/Accounts/Authentication/Config/Internal/Providers/StreamConfigurationProvider.cs new file mode 100644 index 000000000000..934541cd943d --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/StreamConfigurationProvider.cs @@ -0,0 +1,60 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.IO; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + /// + /// Stream based configuration provider + /// + internal abstract class StreamConfigurationProvider : ConfigurationProvider + { + /// + /// The source settings for this provider. + /// + public StreamConfigurationSource Source { get; } + + private bool _loaded; + + /// + /// Constructor. + /// + /// The source. + public StreamConfigurationProvider(StreamConfigurationSource source, string id) : base(id) + { + Source = source ?? throw new ArgumentNullException(nameof(source)); + } + + /// + /// Load the configuration data from the stream. + /// + /// The data stream. + public abstract void Load(Stream stream); + + /// + /// Load the configuration data from the stream. Will throw after the first call. + /// + public override void Load() + { + if (_loaded) + { + throw new InvalidOperationException("StreamConfigurationProviders cannot be loaded more than once."); + } + Load(Source.Stream); + _loaded = true; + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Providers/StreamConfigurationSource.cs b/src/Accounts/Authentication/Config/Internal/Providers/StreamConfigurationSource.cs new file mode 100644 index 000000000000..388cae463514 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/StreamConfigurationSource.cs @@ -0,0 +1,37 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System.IO; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + /// + /// Stream based . + /// + internal abstract class StreamConfigurationSource : IConfigurationSource + { + /// + /// The stream containing the configuration data. + /// + public Stream Stream { get; set; } + + /// + /// Builds the for this source. + /// + /// The . + /// An + public abstract IConfigurationProvider Build(IConfigurationBuilder builder, string id); + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Providers/UnsettableMemoryConfigurationBuilderExtensions.cs b/src/Accounts/Authentication/Config/Internal/Providers/UnsettableMemoryConfigurationBuilderExtensions.cs new file mode 100644 index 000000000000..dee76dc0c09e --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/UnsettableMemoryConfigurationBuilderExtensions.cs @@ -0,0 +1,33 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + internal static class UnsettableMemoryConfigurationBuilderExtensions + { + public static IConfigurationBuilder AddUnsettableInMemoryCollection(this IConfigurationBuilder builder, string id) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Add(id, new UnsettableMemoryConfigurationSource()); + return builder; + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Providers/UnsettableMemoryConfigurationProvider.cs b/src/Accounts/Authentication/Config/Internal/Providers/UnsettableMemoryConfigurationProvider.cs new file mode 100644 index 000000000000..889e544bd758 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/UnsettableMemoryConfigurationProvider.cs @@ -0,0 +1,81 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + /// + /// In-memory implementation of + /// + internal class UnsettableMemoryConfigurationProvider : ConfigurationProvider, IEnumerable> + { + private readonly UnsettableMemoryConfigurationSource _source; + + /// + /// Initialize a new instance from the source. + /// + /// The source settings. + public UnsettableMemoryConfigurationProvider(UnsettableMemoryConfigurationSource source, string id): base(id) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + _source = source; + } + + /// + /// Add a new key and value pair. + /// + /// The configuration key. + /// The configuration value. + public void Add(string key, string value) + { + Data.Add(key, value); + } + + public void Unset(string key) + { + Data.Remove(key); + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that can be used to iterate through the collection. + public IEnumerator> GetEnumerator() + { + return Data.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void UnsetAll() + { + Data.Clear(); + } + } +} diff --git a/src/Accounts/Authentication/Config/Internal/Providers/UnsettableMemoryConfigurationSource.cs b/src/Accounts/Authentication/Config/Internal/Providers/UnsettableMemoryConfigurationSource.cs new file mode 100644 index 000000000000..f4c8f8892821 --- /dev/null +++ b/src/Accounts/Authentication/Config/Internal/Providers/UnsettableMemoryConfigurationSource.cs @@ -0,0 +1,26 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers +{ + internal class UnsettableMemoryConfigurationSource : IConfigurationSource + { + public IConfigurationProvider Build(IConfigurationBuilder builder, string id) + { + return new UnsettableMemoryConfigurationProvider(this, id); + } + } +} From e54b3cbec5b7088174620f26b70d15996bd379d2 Mon Sep 17 00:00:00 2001 From: Yeming Liu <11371776+isra-fel@users.noreply.github.com> Date: Thu, 24 Mar 2022 16:40:30 +0800 Subject: [PATCH 2/9] implementation of config framework --- .../Authentication/Authentication.csproj | 4 + .../Authentication/AzureSessionInitializer.cs | 14 +- .../Config/ConfigInitializer.cs | 156 ++++++ .../Authentication/Config/ConfigManager.cs | 468 ++++++++++++++++++ .../Config/Helper/AppliesToHelper.cs | 99 ++++ .../Config/Helper/ConfigPathHelper.cs | 59 +++ .../Config/Helper/ConfigScopeHelper.cs | 40 ++ .../DefaultEnvironmentVariableProvider.cs | 35 ++ .../Config/Helper/JsonConfigWriter.cs | 153 ++++++ .../Config/Models/InternalInvocationInfo.cs | 35 ++ .../Config/Models/InvocationInfoAdapter.cs | 25 + .../Authentication/Config/Models/PSConfig.cs | 42 ++ .../Config/Models/SimpleTypedConfig.cs | 50 ++ .../Config/Models/TypedConfig.cs | 69 +++ src/Accounts/Authentication/Constants.cs | 13 + 15 files changed, 1261 insertions(+), 1 deletion(-) create mode 100644 src/Accounts/Authentication/Config/ConfigInitializer.cs create mode 100644 src/Accounts/Authentication/Config/ConfigManager.cs create mode 100644 src/Accounts/Authentication/Config/Helper/AppliesToHelper.cs create mode 100644 src/Accounts/Authentication/Config/Helper/ConfigPathHelper.cs create mode 100644 src/Accounts/Authentication/Config/Helper/ConfigScopeHelper.cs create mode 100644 src/Accounts/Authentication/Config/Helper/DefaultEnvironmentVariableProvider.cs create mode 100644 src/Accounts/Authentication/Config/Helper/JsonConfigWriter.cs create mode 100644 src/Accounts/Authentication/Config/Models/InternalInvocationInfo.cs create mode 100644 src/Accounts/Authentication/Config/Models/InvocationInfoAdapter.cs create mode 100644 src/Accounts/Authentication/Config/Models/PSConfig.cs create mode 100644 src/Accounts/Authentication/Config/Models/SimpleTypedConfig.cs create mode 100644 src/Accounts/Authentication/Config/Models/TypedConfig.cs diff --git a/src/Accounts/Authentication/Authentication.csproj b/src/Accounts/Authentication/Authentication.csproj index 388c5e5f8e2e..e37d65960075 100644 --- a/src/Accounts/Authentication/Authentication.csproj +++ b/src/Accounts/Authentication/Authentication.csproj @@ -30,4 +30,8 @@ + + + + diff --git a/src/Accounts/Authentication/AzureSessionInitializer.cs b/src/Accounts/Authentication/AzureSessionInitializer.cs index 0d5ae62ef5ae..6d3c5c84e692 100644 --- a/src/Accounts/Authentication/AzureSessionInitializer.cs +++ b/src/Accounts/Authentication/AzureSessionInitializer.cs @@ -24,10 +24,11 @@ using Microsoft.Azure.Commands.Common.Authentication.Authentication.TokenCache; using Microsoft.Azure.Commands.Common.Authentication.Factories; using Microsoft.Azure.Commands.Common.Authentication.Properties; - +using Microsoft.Azure.Commands.Common.Authentication.Config; using Newtonsoft.Json; using TraceLevel = System.Diagnostics.TraceLevel; +using System.Collections.Generic; namespace Microsoft.Azure.Commands.Common.Authentication { @@ -244,12 +245,23 @@ static IAzureSession CreateInstance(IDataStore dataStore = null) session.TokenCacheDirectory = autoSave.CacheDirectory; session.TokenCacheFile = autoSave.CacheFile; + InitializeConfigs(session); InitializeDataCollection(session); session.RegisterComponent(HttpClientOperationsFactory.Name, () => HttpClientOperationsFactory.Create()); session.TokenCache = session.TokenCache ?? new AzureTokenCache(); return session; } + private static void InitializeConfigs(AzureSession session) + { + var fallbackList = new List() + { + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".Azure", "PSConfig.json"), + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ".Azure", "PSConfig.json") + }; + new ConfigInitializer(fallbackList).InitializeForAzureSession(session); + } + public class AdalSession : AzureSession { #if !NETSTANDARD diff --git a/src/Accounts/Authentication/Config/ConfigInitializer.cs b/src/Accounts/Authentication/Config/ConfigInitializer.cs new file mode 100644 index 000000000000..d179bbe9fc89 --- /dev/null +++ b/src/Accounts/Authentication/Config/ConfigInitializer.cs @@ -0,0 +1,156 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Abstractions; +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using Microsoft.Azure.PowerShell.Common.Config; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + /// + /// Initializes the config file and config manager. + /// + internal class ConfigInitializer + { + internal IDataStore DataStore { get; set; } = new DiskDataStore(); + private static readonly object _fsLock = new object(); + + internal IEnvironmentVariableProvider EnvironmentVariableProvider { get; set; } = new DefaultEnvironmentVariableProvider(); + + private readonly string _pathToConfigFile; + + public ConfigInitializer(IEnumerable paths) + { + _ = paths ?? throw new ArgumentNullException(nameof(paths)); + _pathToConfigFile = GetPathToConfigFile(paths); + } + + /// + /// Loop through the fallback list of paths of the config file. Returns the first usable one. + /// + /// A list of paths to the config file. When one is not usable, it will fallback to the next. + /// + /// When no one in the list is usable. + private string GetPathToConfigFile(IEnumerable paths) + { + // find first exist path and use it + foreach (string path in paths) + { + if (DataStore.FileExists(path)) + { + return path; + } + } + // if not found, use the first writable path + foreach (string path in paths) + { + try + { + DirectoryInfo dir = new FileInfo(path).Directory; + DataStore.CreateDirectory(dir.FullName); // create directory if not exists + using (var _ = DataStore.OpenForExclusiveWrite(path)) { } + return path; + } + catch (Exception) + { + continue; + } + } + throw new ApplicationException($"Failed to store the config file. Please make sure any one of the following paths is accessible: {string.Join(", ", paths)}"); + } + + internal IConfigManager GetConfigManager() + { + lock (_fsLock) + { + ValidateConfigFile(); + } + return new ConfigManager(_pathToConfigFile, DataStore, EnvironmentVariableProvider); + } + + private void ValidateConfigFileContent() + { + string json = DataStore.ReadFileAsText(_pathToConfigFile); + + bool isValidJson = true; + try + { + JObject.Parse(json); + } + catch (Exception) + { + isValidJson = false; + } + + if (string.IsNullOrEmpty(json) || !isValidJson) + { + Debug.Write($"[ConfigInitializer] Failed to parse the config file at {_pathToConfigFile}. Clearing the file."); + ResetConfigFileToDefault(); + } + } + + private void ValidateConfigFile() + { + if (!DataStore.FileExists(_pathToConfigFile)) + { + ResetConfigFileToDefault(); + } + else + { + ValidateConfigFileContent(); + } + } + + private void ResetConfigFileToDefault() + { + try + { + DataStore.WriteFile(_pathToConfigFile, @"{}"); + } + catch (Exception ex) + { + // do not halt for IO exception + Debug.WriteLine(ex.Message); + } + } + + // todo: tests initializes configs in a different way. Maybe there should be an abstraction IConfigInitializer and two concrete classes ConfigInitializer + TestConfigInitializer + internal void InitializeForAzureSession(AzureSession session) + { + IConfigManager configManager = GetConfigManager(); + session.RegisterComponent(nameof(IConfigManager), () => configManager); + RegisterConfigs(configManager); + configManager.BuildConfig(); + } + + private void RegisterConfigs(IConfigManager configManager) + { + // simple configs + // todo: review the help messages + //configManager.RegisterConfig(new SimpleTypedConfig(ConfigKeys.SuppressWarningMessage, "Controls if the warning messages of upcoming breaking changes are enabled or suppressed. The messages are typically displayed when a cmdlet that will have breaking change in the future is executed.", false, BreakingChangeAttributeHelper.SUPPRESS_ERROR_OR_WARNING_MESSAGE_ENV_VARIABLE_NAME)); + //configManager.RegisterConfig(new SimpleTypedConfig(ConfigKeys.EnableInterceptSurvey, "When enabled, a message of taking part in the survey about the user experience of Azure PowerShell will prompt at low frequency.", true, "Azure_PS_Intercept_Survey")); + // todo: when the input is not a valid subscription name or id. Connect-AzAccount will throw an error. Is it right? + //configManager.RegisterConfig(new SimpleTypedConfig(ConfigKeys.DefaultSubscriptionForLogin, "Subscription name or GUID. If defined, when logging in Azure PowerShell without specifying the subscription, this one will be used to select the default context.", string.Empty)); + // todo: add later + //configManager.RegisterConfig(new RetryConfig()); + // todo: how to migrate old config + //configManager.RegisterConfig(new EnableDataCollectionConfig()); + } + } +} diff --git a/src/Accounts/Authentication/Config/ConfigManager.cs b/src/Accounts/Authentication/Config/ConfigManager.cs new file mode 100644 index 000000000000..7b906af858a9 --- /dev/null +++ b/src/Accounts/Authentication/Config/ConfigManager.cs @@ -0,0 +1,468 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Abstractions; +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal; +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Providers; +using Microsoft.Azure.Commands.Common.Exceptions; +using Microsoft.Azure.Commands.ResourceManager.Common; +using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.WindowsAzure.Commands.Utilities.Common; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Management.Automation; +using System.Threading; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + /// + /// Default implementation of , providing CRUD abilities to the configs. + /// + internal class ConfigManager : IConfigManager + { + /// + public string ConfigFilePath { get; private set; } + + private IConfigurationRoot _root; + private readonly ConcurrentDictionary _configDefinitionMap = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + private IOrderedEnumerable> OrderedConfigDefinitionMap => _configDefinitionMap.OrderBy(x => x.Key); + private readonly ConcurrentDictionary EnvironmentVariableToKeyMap = new ConcurrentDictionary(); + private readonly IEnvironmentVariableProvider _environmentVariableProvider; + private readonly IDataStore _dataStore; + private readonly JsonConfigWriter _jsonConfigWriter; + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + + /// + /// Creates an instance of . + /// + /// Path to the config file. + /// Provider of file system APIs. + /// Provider of environment variable APIs. + internal ConfigManager(string configFilePath, IDataStore dataStore, IEnvironmentVariableProvider environmentVariableProvider) + { + _ = dataStore ?? throw new AzPSArgumentNullException($"{nameof(dataStore)} cannot be null.", nameof(dataStore)); + _ = configFilePath ?? throw new AzPSArgumentNullException($"{nameof(configFilePath)} cannot be null.", nameof(configFilePath)); + _ = environmentVariableProvider ?? throw new AzPSArgumentNullException($"{nameof(environmentVariableProvider)} cannot be null.", nameof(environmentVariableProvider)); + ConfigFilePath = configFilePath; + _environmentVariableProvider = environmentVariableProvider; + _dataStore = dataStore; + _jsonConfigWriter = new JsonConfigWriter(ConfigFilePath, _dataStore); + } + + /// + /// Rebuild config hierarchy and load from the providers. + /// + public void BuildConfig() + { + var builder = new ConfigurationBuilder(); + + if (SharedUtilities.IsWindowsPlatform()) + { + // User and machine level environment variables are only on Windows + builder.AddEnvironmentVariables(Constants.ConfigProviderIds.MachineEnvironment, new EnvironmentVariablesConfigurationOptions() + { + EnvironmentVariableProvider = _environmentVariableProvider, + EnvironmentVariableTarget = EnvironmentVariableTarget.Machine, + EnvironmentVariableToKeyMap = EnvironmentVariableToKeyMap + }) + .AddEnvironmentVariables(Constants.ConfigProviderIds.UserEnvironment, new EnvironmentVariablesConfigurationOptions() + { + EnvironmentVariableProvider = _environmentVariableProvider, + EnvironmentVariableTarget = EnvironmentVariableTarget.User, + EnvironmentVariableToKeyMap = EnvironmentVariableToKeyMap + }); + } + builder.AddJsonStream(Constants.ConfigProviderIds.UserConfig, _dataStore.ReadFileAsStream(ConfigFilePath)) + .AddEnvironmentVariables(Constants.ConfigProviderIds.ProcessEnvironment, new EnvironmentVariablesConfigurationOptions() + { + EnvironmentVariableProvider = _environmentVariableProvider, + EnvironmentVariableTarget = EnvironmentVariableTarget.Process, + EnvironmentVariableToKeyMap = EnvironmentVariableToKeyMap + }) + .AddUnsettableInMemoryCollection(Constants.ConfigProviderIds.ProcessConfig); + + _lock.EnterReadLock(); + try + { + _root = builder.Build(); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + public void RegisterConfig(ConfigDefinition config) + { + // check if key already taken + if (_configDefinitionMap.ContainsKey(config.Key)) + { + if (_configDefinitionMap[config.Key] == config) + { + Debug.WriteLine($"Config with key [{config.Key}] was registered twice"); + } + else + { + throw new AzPSArgumentException($"Duplicated config key. [{config.Key}] was already taken.", nameof(config.Key)); + } + return; + } + // configure environment variable providers + if (!string.IsNullOrEmpty(config.EnvironmentVariableName)) + { + EnvironmentVariableToKeyMap[config.EnvironmentVariableName] = ConfigPathHelper.GetPathOfConfig(config.Key); + } + _configDefinitionMap[config.Key] = config; + } + + /// + public T GetConfigValue(string key, object invocation = null) + { + if (invocation != null && !(invocation is InvocationInfo)) + { + throw new AzPSArgumentException($"Type error: type of {nameof(invocation)} must be {nameof(InvocationInfo)}", nameof(invocation)); + } + return GetConfigValueInternal(key, new InvocationInfoAdapter((InvocationInfo)invocation)); + } + + internal T GetConfigValueInternal(string key, InternalInvocationInfo invocation) => (T)GetConfigValueInternal(key, invocation); + + internal object GetConfigValueInternal(string key, InternalInvocationInfo invocation) + { + _ = key ?? throw new AzPSArgumentNullException($"{nameof(key)} cannot be null.", nameof(key)); + if (!_configDefinitionMap.TryGetValue(key, out ConfigDefinition definition) || definition == null) + { + throw new AzPSArgumentException($"Config with key [{key}] was not registered.", nameof(key)); + } + + foreach (var path in ConfigPathHelper.EnumerateConfigPaths(key, invocation)) + { + IConfigurationSection section = _root.GetSection(path); + if (section.Exists()) + { + (object value, _) = GetConfigValueOrDefault(section, definition); + WriteDebug($"[ConfigManager] Got [{value}] from [{key}], Module = [{invocation?.ModuleName}], Cmdlet = [{invocation?.CmdletName}]."); + return value; + } + } + + WriteDebug($"[ConfigManager] Got nothing from [{key}], Module = [{invocation?.ModuleName}], Cmdlet = [{invocation?.CmdletName}]. Returning default value [{definition.DefaultValue}]."); + return definition.DefaultValue; + } + + private void WriteDebug(string message) + { + WriteMessage(message, AzureRMCmdlet.WriteDebugKey); + } + + private void WriteMessage(string message, string eventHandlerKey) + { + try + { + if (AzureSession.Instance.TryGetComponent(eventHandlerKey, out EventHandler writeDebug)) + { + writeDebug.Invoke(this, new StreamEventArgs() { Message = message }); + } + } + catch (Exception) + { + // do not throw when session is not initialized + } + } + + private void WriteWarning(string message) + { + WriteMessage(message, AzureRMCmdlet.WriteWarningKey); + } + + /// + public IEnumerable ListConfigDefinitions() + { + return OrderedConfigDefinitionMap.Select(x => x.Value); + } + + /// + public IEnumerable ListConfigs(ConfigFilter filter = null) + { + IList results = new List(); + + // include all values + ISet noNeedForDefault = new HashSet(); + foreach (var appliesToSection in _root.GetChildren()) + { + foreach (var configSection in appliesToSection.GetChildren()) + { + string key = configSection.Key; + if (_configDefinitionMap.TryGetValue(key, out var configDefinition)) + { + (object value, string providerId) = GetConfigValueOrDefault(configSection, configDefinition); + ConfigScope scope = ConfigScopeHelper.GetScopeByProviderId(providerId); + results.Add(new ConfigData(configDefinition, value, scope, appliesToSection.Key)); + // if a config is already set at global level, there's no need to return its default value + if (string.Equals(ConfigFilter.GlobalAppliesTo, appliesToSection.Key, StringComparison.OrdinalIgnoreCase)) + { + noNeedForDefault.Add(configDefinition.Key); + } + } + } + } + + // include default values + IEnumerable keys = filter?.Keys ?? Enumerable.Empty(); + bool isRegisteredKey(string key) => _configDefinitionMap.Keys.Contains(key, StringComparer.OrdinalIgnoreCase); + IEnumerable configDefinitions = keys.Any() ? keys.Where(isRegisteredKey).Select(key => _configDefinitionMap[key]) : OrderedConfigDefinitionMap.Select(x => x.Value); + configDefinitions.Where(x => !noNeedForDefault.Contains(x.Key)).Select(x => GetDefaultConfigData(x)).ForEach(x => results.Add(x)); + + + if (keys.Any()) + { + results = results.Where(x => keys.Contains(x.Definition.Key, StringComparer.OrdinalIgnoreCase)).ToList(); + } + + string appliesTo = filter?.AppliesTo; + if (!string.IsNullOrEmpty(appliesTo)) + { + results = results.Where(x => string.Equals(appliesTo, x.AppliesTo, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + return results; + } + + /// + /// Get the value and the ID of the corresponding provider of the config. + /// + /// The section that stores the config. + /// The definition of the config. + /// A tuple containing the value of the config and the ID of the provider from which the value is got. + /// Exceptions are handled gracefully in this method. + private (object value, string providerId) GetConfigValueOrDefault(IConfigurationSection section, ConfigDefinition definition) + { + try + { + return section.Get(definition.ValueType); + } + catch (InvalidOperationException ex) + { + WriteWarning($"[ConfigManager] Failed to get value for [{definition.Key}]. Using the default value [{definition.DefaultValue}] instead. Error: {ex.Message}. {ex.InnerException?.Message}"); + WriteDebug($"[ConfigManager] Exception: {ex.Message}, stack trace: \n{ex.StackTrace}"); + return (definition.DefaultValue, Constants.ConfigProviderIds.None); + } + } + + private ConfigData GetDefaultConfigData(ConfigDefinition configDefinition) + { + return new ConfigData(configDefinition, + configDefinition.DefaultValue, + ConfigScope.Default, + ConfigFilter.GlobalAppliesTo); + } + + // A bulk update API is currently unnecessary as we don't expect users to do that. + // But if telemetry data proves it's a demanded feature, we might add it in the future. + // public IEnumerable UpdateConfigs(IEnumerable updateConfigOptions) => updateConfigOptions.Select(UpdateConfig); + + /// + public ConfigData UpdateConfig(string key, object value, ConfigScope scope) + { + return UpdateConfig(new UpdateConfigOptions(key, value, scope)); + } + + /// + public ConfigData UpdateConfig(UpdateConfigOptions options) + { + if (options == null) + { + throw new AzPSArgumentNullException($"{nameof(options)} cannot be null when updating config.", nameof(options)); + } + + if (!_configDefinitionMap.TryGetValue(options.Key, out ConfigDefinition definition) || definition == null) + { + throw new AzPSArgumentException($"Config with key [{options.Key}] was not registered.", nameof(options.Key)); + } + + try + { + definition.Validate(options.Value); + } + catch (Exception e) + { + throw new AzPSArgumentException(e.Message, e); + } + + if (AppliesToHelper.TryParseAppliesTo(options.AppliesTo, out var appliesTo) && !definition.CanApplyTo.Contains(appliesTo)) + { + throw new AzPSArgumentException($"[{options.AppliesTo}] is not a valid value for AppliesTo - it doesn't match any of ({AppliesToHelper.FormatOptions(definition.CanApplyTo)}).", nameof(options.AppliesTo)); + } + + definition.Apply(options.Value); + + string path = ConfigPathHelper.GetPathOfConfig(options.Key, options.AppliesTo); + + switch (options.Scope) + { + case ConfigScope.Process: + SetProcessLevelConfig(path, options.Value); + break; + case ConfigScope.CurrentUser: + SetUserLevelConfig(path, options.Value); + break; + } + + WriteDebug($"[ConfigManager] Updated [{options.Key}] to [{options.Value}]. Scope = [{options.Scope}], AppliesTo = [{options.AppliesTo}]"); + + return new ConfigData(definition, options.Value, options.Scope, options.AppliesTo); + } + + private void SetProcessLevelConfig(string path, object value) + { + GetProcessLevelConfigProvider().Set(path, value.ToString()); + } + + private UnsettableMemoryConfigurationProvider GetProcessLevelConfigProvider() + { + return _root.GetConfigurationProvider(Constants.ConfigProviderIds.ProcessConfig) as UnsettableMemoryConfigurationProvider; + } + + private void SetUserLevelConfig(string path, object value) + { + _lock.EnterWriteLock(); + try + { + _jsonConfigWriter.Update(path, value); + } + finally + { + _lock.ExitWriteLock(); + } + BuildConfig(); // reload the config values + } + + /// + public void ClearConfig(string key, ConfigScope scope) => ClearConfig(new ClearConfigOptions(key, scope)); + + /// + public void ClearConfig(ClearConfigOptions options) + { + _ = options ?? throw new AzPSArgumentNullException($"{nameof(options)} cannot be null.", nameof(options)); + + bool clearAll = string.IsNullOrEmpty(options.Key); + + if (clearAll) + { + ClearAllConfigs(options); + } + else + { + ClearConfigByKey(options); + } + } + + private void ClearAllConfigs(ClearConfigOptions options) + { + switch (options.Scope) + { + case ConfigScope.Process: + ClearProcessLevelAllConfigs(options); + break; + case ConfigScope.CurrentUser: + ClearUserLevelAllConfigs(options); + break; + default: + throw new AzPSArgumentException($"[{options.Scope}] is not a valid scope when clearing configs.", nameof(options.Scope)); + } + WriteDebug($"[ConfigManager] Cleared all the configs. Scope = [{options.Scope}]."); + } + + private void ClearProcessLevelAllConfigs(ClearConfigOptions options) + { + var configProvider = GetProcessLevelConfigProvider(); + if (string.IsNullOrEmpty(options.AppliesTo)) + { + configProvider.UnsetAll(); + } + else + { + foreach (var key in _configDefinitionMap.Keys) + { + configProvider.Unset(ConfigPathHelper.GetPathOfConfig(key, options.AppliesTo)); + } + } + } + + private void ClearUserLevelAllConfigs(ClearConfigOptions options) + { + _lock.EnterWriteLock(); + try + { + if (string.IsNullOrEmpty(options.AppliesTo)) + { + _jsonConfigWriter.ClearAll(); + } + else + { + foreach (var key in _configDefinitionMap.Keys) + { + _jsonConfigWriter.Clear(ConfigPathHelper.GetPathOfConfig(key, options.AppliesTo)); + } + } + } + finally + { + _lock.ExitWriteLock(); + } + BuildConfig(); + } + + private void ClearConfigByKey(ClearConfigOptions options) + { + if (!_configDefinitionMap.TryGetValue(options.Key, out ConfigDefinition definition)) + { + throw new AzPSArgumentException($"Config with key [{options.Key}] was not registered.", nameof(options.Key)); + } + + string path = ConfigPathHelper.GetPathOfConfig(definition.Key, options.AppliesTo); + + switch (options.Scope) + { + case ConfigScope.Process: + GetProcessLevelConfigProvider().Unset(path); + break; + case ConfigScope.CurrentUser: + ClearUserLevelConfigByKey(path); + break; + } + + WriteDebug($"[ConfigManager] Cleared [{options.Key}]. Scope = [{options.Scope}], AppliesTo = [{options.AppliesTo}]"); + } + + private void ClearUserLevelConfigByKey(string key) + { + _lock.EnterWriteLock(); + try + { + _jsonConfigWriter.Clear(key); + } + finally + { + _lock.ExitWriteLock(); + } + BuildConfig(); + } + } +} diff --git a/src/Accounts/Authentication/Config/Helper/AppliesToHelper.cs b/src/Accounts/Authentication/Config/Helper/AppliesToHelper.cs new file mode 100644 index 000000000000..32902f509faa --- /dev/null +++ b/src/Accounts/Authentication/Config/Helper/AppliesToHelper.cs @@ -0,0 +1,99 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.PowerShell.Common.Config; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + /// + /// Helper class to deal with AppliesTo (how large is the scope that the config affects Azure PowerShell). + /// + public static class AppliesToHelper + { + internal static readonly Regex ModulePattern = new Regex(@"^az\.[a-z]+$", RegexOptions.IgnoreCase); + internal static readonly Regex CmdletPattern = new Regex(@"^[a-z]+-[a-z]+$", RegexOptions.IgnoreCase); + internal static readonly Regex ModuleOrCmdletPattern = new Regex(@"^az\.[a-z]+$|^[a-z]+-[a-z]+$", RegexOptions.IgnoreCase); + + /// + /// Tries to parse a user-input text to an enum. + /// + /// Input from user. + /// Result if successful. + /// True if parsed successfully. + public static bool TryParseAppliesTo(string text, out AppliesTo appliesTo) + { + if (string.IsNullOrEmpty(text) || string.Equals(ConfigFilter.GlobalAppliesTo, text, StringComparison.OrdinalIgnoreCase)) + { + appliesTo = AppliesTo.Az; + return true; + } + + if (ModulePattern.IsMatch(text)) + { + appliesTo = AppliesTo.Module; + return true; + } + + if (CmdletPattern.IsMatch(text)) + { + appliesTo = AppliesTo.Cmdlet; + return true; + } + + appliesTo = AppliesTo.Az; + return false; + } + + /// + /// Gets a comma-divided string for human-readable description of the AppliesTo options. + /// + /// Options of AppliesTo. + /// The formated string. + internal static string FormatOptions(IReadOnlyCollection options) + { + if (options == null || !options.Any()) + { + throw new ArgumentException($"Make sure the config definition has a non-empty {nameof(ConfigDefinition.CanApplyTo)}.", nameof(options)); + } + var sb = new StringBuilder(); + bool isFirst = true; + foreach (var option in options) + { + if (!isFirst) + { + sb.Append(", "); + isFirst = false; + } + switch (option) + { + case AppliesTo.Az: + sb.Append(ConfigFilter.GlobalAppliesTo); + break; + case AppliesTo.Cmdlet: + sb.Append("name of a cmdlet"); + break; + case AppliesTo.Module: + sb.Append("name of a module"); + break; + } + } + return sb.ToString(); + } + } +} diff --git a/src/Accounts/Authentication/Config/Helper/ConfigPathHelper.cs b/src/Accounts/Authentication/Config/Helper/ConfigPathHelper.cs new file mode 100644 index 000000000000..73b2b4b5dbe3 --- /dev/null +++ b/src/Accounts/Authentication/Config/Helper/ConfigPathHelper.cs @@ -0,0 +1,59 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal; +using Microsoft.Azure.PowerShell.Common.Config; +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + /// + /// Helper class to deal with the full path where configs are stored. + /// + internal static class ConfigPathHelper + { + /// + /// Gets a list of paths to check when getting a config value by key and invocation info. + /// + /// The key in the config definition. + /// Command invocation info, containing command name and module name. + public static IEnumerable EnumerateConfigPaths(string key, InternalInvocationInfo invocation = null) + { + if (!string.IsNullOrEmpty(invocation?.CmdletName)) + { + yield return GetPathOfConfig(key, invocation.CmdletName); + } + if (!string.IsNullOrEmpty(invocation?.ModuleName)) + { + yield return GetPathOfConfig(key, invocation.ModuleName); + } + yield return GetPathOfConfig(key); + } + + /// + /// Get the path (full key) of a config by its key and what it applies to. + /// + /// + /// Global appliesTo by default. + /// + internal static string GetPathOfConfig(string key, string appliesTo = null) + { + if (string.IsNullOrEmpty(appliesTo)) + { + appliesTo = ConfigFilter.GlobalAppliesTo; + } + return appliesTo + ConfigurationPath.KeyDelimiter + key; + } + } +} diff --git a/src/Accounts/Authentication/Config/Helper/ConfigScopeHelper.cs b/src/Accounts/Authentication/Config/Helper/ConfigScopeHelper.cs new file mode 100644 index 000000000000..16165b01cbf2 --- /dev/null +++ b/src/Accounts/Authentication/Config/Helper/ConfigScopeHelper.cs @@ -0,0 +1,40 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Exceptions; +using Microsoft.Azure.PowerShell.Common.Config; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + internal static class ConfigScopeHelper + { + public static ConfigScope GetScopeByProviderId(string id) + { + switch (id) + { + case Constants.ConfigProviderIds.MachineEnvironment: + case Constants.ConfigProviderIds.UserEnvironment: + case Constants.ConfigProviderIds.UserConfig: + return ConfigScope.CurrentUser; + case Constants.ConfigProviderIds.ProcessEnvironment: + case Constants.ConfigProviderIds.ProcessConfig: + return ConfigScope.Process; + case Constants.ConfigProviderIds.None: + return ConfigScope.Default; + default: + throw new AzPSArgumentOutOfRangeException($"Unexpected provider ID [{id}]. See {nameof(Constants.ConfigProviderIds)} class for all valid IDs.", nameof(id)); + } + } + } +} diff --git a/src/Accounts/Authentication/Config/Helper/DefaultEnvironmentVariableProvider.cs b/src/Accounts/Authentication/Config/Helper/DefaultEnvironmentVariableProvider.cs new file mode 100644 index 000000000000..f93c1f373940 --- /dev/null +++ b/src/Accounts/Authentication/Config/Helper/DefaultEnvironmentVariableProvider.cs @@ -0,0 +1,35 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; +using System; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + /// + /// Default implementation of that utilizes the API. + /// + internal class DefaultEnvironmentVariableProvider : IEnvironmentVariableProvider + { + public string Get(string variableName, EnvironmentVariableTarget target = EnvironmentVariableTarget.Process) + { + return Environment.GetEnvironmentVariable(variableName, target); + } + + public void Set(string variableName, string value, EnvironmentVariableTarget target = EnvironmentVariableTarget.Process) + { + Environment.SetEnvironmentVariable(variableName, value, target); + } + } +} diff --git a/src/Accounts/Authentication/Config/Helper/JsonConfigWriter.cs b/src/Accounts/Authentication/Config/Helper/JsonConfigWriter.cs new file mode 100644 index 000000000000..841bb0abede5 --- /dev/null +++ b/src/Accounts/Authentication/Config/Helper/JsonConfigWriter.cs @@ -0,0 +1,153 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.IO; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + /// + /// Helper for updating the config JSON file. + /// + internal class JsonConfigWriter + { + private readonly string _jsonConfigPath; + private readonly IDataStore _dataStore; + + public JsonConfigWriter(string jsonConfigPath, IDataStore dataStore) + { + _jsonConfigPath = jsonConfigPath; + _dataStore = dataStore; + } + + /// + /// Update a config value. + /// + /// The full path of the config. + /// The value to update. + internal void Update(string key, object value) => TryUpdate(key, true, (JObject parent, string propertyName) => + { + var prop = parent.Property(propertyName); + + if (prop == null) + { + prop = new JProperty(propertyName, value); + + parent.Add(prop); + } + else + { + prop.Value = IsMultiContent(value) ? new JArray(value) : JToken.FromObject(value); + } + }); + + private bool IsMultiContent(object value) + { + return value is Array; + } + + /// + /// Locates the node by key in the JSON object, and performs a general update (add, modify or remove a property). + /// + /// The full path to the config. + /// Whether to create the JSON node when part of the path is missing. + /// The concrete action to perform. First argument is the parent node in the JSON object, second is the name of the property to update. + /// Whether the update is successful. + private bool TryUpdate(string key, bool createWhenNotExist, Action updateAction) + { + string json = _dataStore.ReadFileAsText(_jsonConfigPath); + JObject root = JObject.Parse(json); + + string[] segments = key.Split(ConfigurationPath.KeyDelimiter.ToCharArray()); + JObject parent = LocateParentNode(root, segments, createWhenNotExist); + if (parent == null) + { + return false; + } + + string propertyName = segments[segments.Length - 1]; + + updateAction(parent, propertyName); + + // hack: to avoid last version of the config remaining in the file, empty it first + _dataStore.WriteFile(_jsonConfigPath, string.Empty); + + JsonSerializer serializer = new JsonSerializer + { + Formatting = Formatting.Indented + }; + using (Stream fs = _dataStore.OpenForExclusiveWrite(_jsonConfigPath)) + using (StreamWriter sw = new StreamWriter(fs)) + using (var writer = new JsonTextWriter(sw) { Indentation = 4 }) + { + serializer.Serialize(writer, root); + } + + return true; + } + + private static JObject LocateParentNode(JObject root, string[] segments, bool createWhenNotExist) + { + JObject node = root; + for (int i = 0; i < segments.Length - 1; ++i) + { + string segment = segments[i]; + // JObject.TryGetValue() supports case insensitivity + // otherwise we might get duplicated keys with different casing in the config file + if (node.TryGetValue(segment, StringComparison.OrdinalIgnoreCase, out JToken match)) + { + node = (JObject)match; + } + else + { + if (createWhenNotExist) + { + node[segment] = new JObject(); + node = (JObject)node[segment]; + } + else + { + return null; + } + } + } + + return node; + } + + /// + /// Clear a config by key. + /// + /// The full path to the config. + internal void Clear(string key) => TryUpdate(key, false, (parent, propertyName) => + { + if (parent.Property(propertyName) != null) + { + parent.Remove(propertyName); + } + // if the config is never set, there's no need to clear. + }); + + /// + /// Clear all the configs. + /// + internal void ClearAll() + { + _dataStore.WriteFile(_jsonConfigPath, @"{}"); + } + } +} diff --git a/src/Accounts/Authentication/Config/Models/InternalInvocationInfo.cs b/src/Accounts/Authentication/Config/Models/InternalInvocationInfo.cs new file mode 100644 index 000000000000..fe4fdae78343 --- /dev/null +++ b/src/Accounts/Authentication/Config/Models/InternalInvocationInfo.cs @@ -0,0 +1,35 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + /// + /// Abstraction for the PS InvocationInfo. + /// + internal class InternalInvocationInfo + { + public InternalInvocationInfo() : this(null, null) + { + } + + public InternalInvocationInfo(string moduleName, string cmdletName) + { + CmdletName = cmdletName; + ModuleName = moduleName; + } + + public string ModuleName { get; set; } = null; + public string CmdletName { get; set; } = null; + } +} \ No newline at end of file diff --git a/src/Accounts/Authentication/Config/Models/InvocationInfoAdapter.cs b/src/Accounts/Authentication/Config/Models/InvocationInfoAdapter.cs new file mode 100644 index 000000000000..b304ed5c2879 --- /dev/null +++ b/src/Accounts/Authentication/Config/Models/InvocationInfoAdapter.cs @@ -0,0 +1,25 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Management.Automation; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + internal class InvocationInfoAdapter : InternalInvocationInfo + { + public InvocationInfoAdapter(InvocationInfo invocationInfo) : base( + invocationInfo?.MyCommand?.ModuleName, invocationInfo?.MyCommand?.Name) + { } + } +} diff --git a/src/Accounts/Authentication/Config/Models/PSConfig.cs b/src/Accounts/Authentication/Config/Models/PSConfig.cs new file mode 100644 index 000000000000..782608543971 --- /dev/null +++ b/src/Accounts/Authentication/Config/Models/PSConfig.cs @@ -0,0 +1,42 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.PowerShell.Common.Config; + +/// +/// The output model of config-related cmdlets. +/// +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + public class PSConfig + { + public string Key { get; } + public object Value { get; } + public ConfigScope Scope { get; } = ConfigScope.CurrentUser; + public string AppliesTo { get; } + public string HelpMessage { get; } + public object DefaultValue { get; } + public PSConfig(ConfigData config) + { + Value = config.Value; + Scope = config.Scope; + AppliesTo = config.AppliesTo; + + var def = config.Definition; + Key = def.Key; + HelpMessage = def.HelpMessage; + DefaultValue = def.DefaultValue; + } + } +} diff --git a/src/Accounts/Authentication/Config/Models/SimpleTypedConfig.cs b/src/Accounts/Authentication/Config/Models/SimpleTypedConfig.cs new file mode 100644 index 000000000000..8300fe8b445e --- /dev/null +++ b/src/Accounts/Authentication/Config/Models/SimpleTypedConfig.cs @@ -0,0 +1,50 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.PowerShell.Common.Config; +using System.Collections.Generic; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + /// + /// Represents a simple typed config. For complex configs please define your own type inheriting or . + /// + /// Type of the config value. + internal class SimpleTypedConfig : TypedConfig + { + private readonly string _key; + private readonly string _helpMessage; + private readonly TValue _defaultValue; + private readonly string _environmentVariable; + private readonly IReadOnlyCollection _canApplyTo = null; + + public SimpleTypedConfig(string key, string helpMessage, TValue defaultValue, string environmentVariable = null, IReadOnlyCollection canApplyTo = null) + { + _key = key; + _helpMessage = helpMessage; + _defaultValue = defaultValue; + _environmentVariable = environmentVariable; + _canApplyTo = canApplyTo; + } + + public override string Key => _key; + public override string HelpMessage => _helpMessage; + public override object DefaultValue => _defaultValue; + public override string EnvironmentVariableName => _environmentVariable; + public override IReadOnlyCollection CanApplyTo + { + get { return _canApplyTo ?? base.CanApplyTo; } + } + } +} diff --git a/src/Accounts/Authentication/Config/Models/TypedConfig.cs b/src/Accounts/Authentication/Config/Models/TypedConfig.cs new file mode 100644 index 000000000000..8c67463f1e71 --- /dev/null +++ b/src/Accounts/Authentication/Config/Models/TypedConfig.cs @@ -0,0 +1,69 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.PowerShell.Common.Config; +using System; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + /// + /// Base class for configs that have a typed value. + /// + /// The type of the config value. + internal abstract class TypedConfig : ConfigDefinition + { + protected TypedConfig() + { + } + + public TValue TypedDefaultValue => (TValue)DefaultValue; + + /// + /// Validates if the input value is type . + /// + /// The value to check. + /// Throws when the value in another type. + public override void Validate(object value) + { + base.Validate(value); + if (!(value is TValue)) + { + throw new ArgumentException($"Unexpected value type [{value.GetType()}]. The value of config [{Key}] should be of type [{ValueType}]", nameof(value)); + } + } + + /// + /// Performs side effects before applying the config. + /// + /// The value to be applied to this typed config. + /// + /// This method is sealed. + /// Derived types should override . + /// + public override sealed void Apply(object value) + { + base.Apply(value); + ApplyTyped((TValue)value); + } + + /// + /// Generic version of . + /// Override in child classes to perform side effects before applying the config value. + /// + /// The value to be applied to this typed config, cast to the correct type. + protected virtual void ApplyTyped(TValue value) { } + + public override Type ValueType => typeof(TValue); + } +} diff --git a/src/Accounts/Authentication/Constants.cs b/src/Accounts/Authentication/Constants.cs index d4f0256257a7..7b8654fbcb54 100644 --- a/src/Accounts/Authentication/Constants.cs +++ b/src/Accounts/Authentication/Constants.cs @@ -24,5 +24,18 @@ public static class Constants public const string MicrosoftGraphAccessToken = "MicrosoftGraphAccessToken"; public const string DefaultValue = "Default"; + + public class ConfigProviderIds + { + public const string MachineEnvironment = "Environment (Machine)"; + public const string UserEnvironment = "Environment (User)"; + public const string ProcessEnvironment = "Environment (Process)"; + public const string UserConfig = "Config (User)"; + public const string ProcessConfig = "Config (Process)"; + /// + /// Represents that the value is not in any providers. + /// + public const string None = "None"; + } } } From 3f59938167f26f711ce37a768eeb02b0b976d7ca Mon Sep 17 00:00:00 2001 From: Yeming Liu <11371776+isra-fel@users.noreply.github.com> Date: Thu, 24 Mar 2022 16:41:12 +0800 Subject: [PATCH 3/9] cmdlet related --- src/Accounts/Accounts/Accounts.format.ps1xml | 57 +++- src/Accounts/Accounts/Az.Accounts.psd1 | 2 +- .../Accounts/Config/ClearConfigCommand.cs | 110 ++++++++ .../Accounts/Config/ConfigCommandBase.cs | 98 +++++++ .../Accounts/Config/GetConfigCommand.cs | 66 +++++ .../Accounts/Config/UpdateConfigCommand.cs | 75 ++++++ src/Accounts/Accounts/help/Az.Accounts.md | 9 + src/Accounts/Accounts/help/Clear-AzConfig.md | 251 ++++++++++++++++++ src/Accounts/Accounts/help/Get-AzConfig.md | 168 ++++++++++++ src/Accounts/Accounts/help/Update-AzConfig.md | 199 ++++++++++++++ 10 files changed, 1033 insertions(+), 2 deletions(-) create mode 100644 src/Accounts/Accounts/Config/ClearConfigCommand.cs create mode 100644 src/Accounts/Accounts/Config/ConfigCommandBase.cs create mode 100644 src/Accounts/Accounts/Config/GetConfigCommand.cs create mode 100644 src/Accounts/Accounts/Config/UpdateConfigCommand.cs create mode 100644 src/Accounts/Accounts/help/Clear-AzConfig.md create mode 100644 src/Accounts/Accounts/help/Get-AzConfig.md create mode 100644 src/Accounts/Accounts/help/Update-AzConfig.md diff --git a/src/Accounts/Accounts/Accounts.format.ps1xml b/src/Accounts/Accounts/Accounts.format.ps1xml index 818c9fc643a1..b4f4f5ef762a 100644 --- a/src/Accounts/Accounts/Accounts.format.ps1xml +++ b/src/Accounts/Accounts/Accounts.format.ps1xml @@ -276,6 +276,61 @@ - + + Microsoft.Azure.Commands.Common.Authentication.Config.PSConfig + + Microsoft.Azure.Commands.Common.Authentication.Config.PSConfig + + + + + Left + + + + Left + + + + Left + + + + Left + + + + Left + + + + + + + + Left + Key + + + Left + Value + + + Left + AppliesTo + + + Left + Scope + + + Left + HelpMessage + + + + + + diff --git a/src/Accounts/Accounts/Az.Accounts.psd1 b/src/Accounts/Accounts/Az.Accounts.psd1 index 224a2daa0d83..e3dd8a869176 100644 --- a/src/Accounts/Accounts/Az.Accounts.psd1 +++ b/src/Accounts/Accounts/Az.Accounts.psd1 @@ -108,7 +108,7 @@ CmdletsToExport = 'Disable-AzDataCollection', 'Disable-AzContextAutosave', 'Set-AzDefault', 'Get-AzDefault', 'Clear-AzDefault', 'Register-AzModule', 'Enable-AzureRmAlias', 'Disable-AzureRmAlias', 'Uninstall-AzureRm', 'Invoke-AzRestMethod', 'Get-AzAccessToken', - 'Open-AzSurveyLink' + 'Open-AzSurveyLink', 'Get-AzConfig', 'Update-AzConfig', 'Clear-AzConfig' # Variables to export from this module # VariablesToExport = @() diff --git a/src/Accounts/Accounts/Config/ClearConfigCommand.cs b/src/Accounts/Accounts/Config/ClearConfigCommand.cs new file mode 100644 index 000000000000..e5ba6206b004 --- /dev/null +++ b/src/Accounts/Accounts/Config/ClearConfigCommand.cs @@ -0,0 +1,110 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.WindowsAzure.Commands.Utilities.Common; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Management.Automation; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + [Cmdlet("Clear", "AzConfig", SupportsShouldProcess = true)] + [OutputType(typeof(bool))] + public class ClearConfigCommand : ConfigCommandBase, IDynamicParameters + { + private const string ClearByKey = "ClearByKey"; + private const string ClearAll = "ClearAll"; + + private const string ProcessMessage = "Clear the configs that apply to \"{0}\" by the following keys: {1}."; + + private string ContinueMessage => $"Clear all the configs that apply to \"{AppliesTo}\" in scope {Scope}?"; + private string ProcessTarget => $"Configs in scope {Scope}"; + + [Parameter(ParameterSetName = ClearAll, Mandatory = true, HelpMessage = "Clear all configs.")] + public SwitchParameter All { get; set; } + + [Parameter(ParameterSetName = ClearAll, HelpMessage = "Do not ask for confirmation when clearing all configs.")] + public SwitchParameter Force { get; set; } + + [Parameter(HelpMessage = "Returns true if cmdlet executes correctly.")] + public SwitchParameter PassThru { get; set; } + + public new object GetDynamicParameters() + { + return GetDynamicParameters((ConfigDefinition config) => + new RuntimeDefinedParameter( + config.Key, + typeof(SwitchParameter), + new Collection() { + new ParameterAttribute { + ParameterSetName = ClearByKey, + HelpMessage = config.HelpMessage + } + })); + } + + public override void ExecuteCmdlet() + { + switch (ParameterSetName) + { + case ClearByKey: + ClearConfigByKey(); + break; + case ClearAll: + ClearAllConfigs(); + break; + } + if (PassThru) + { + WriteObject(true); + } + } + + private void ClearConfigByKey() + { + IEnumerable configKeysFromInput = GetConfigsSpecifiedByUser().Where(x => (bool)x.Value).Select(x => x.Key); + if (!configKeysFromInput.Any()) + { + WriteWarning($"Please specify the key(s) of the configs to clear. Run `help {MyInvocation.MyCommand.Name}` for more information."); + return; + } + base.ConfirmAction( + string.Format(ProcessMessage, AppliesTo, string.Join(", ", configKeysFromInput)), + ProcessTarget, + () => configKeysFromInput.ForEach(ClearConfigByKey)); + } + + private void ClearConfigByKey(string key) + { + ConfigManager.ClearConfig(new ClearConfigOptions(key, Scope) + { + AppliesTo = AppliesTo + }); + } + + private void ClearAllConfigs() + { + ConfirmAction(Force, ContinueMessage, ContinueMessage, ProcessTarget, () => + { + ConfigManager.ClearConfig(new ClearConfigOptions(null, Scope) + { + AppliesTo = AppliesTo + }); + }); + } + } +} diff --git a/src/Accounts/Accounts/Config/ConfigCommandBase.cs b/src/Accounts/Accounts/Config/ConfigCommandBase.cs new file mode 100644 index 000000000000..dc5b43c06b66 --- /dev/null +++ b/src/Accounts/Accounts/Config/ConfigCommandBase.cs @@ -0,0 +1,98 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Exceptions; +using Microsoft.Azure.Commands.ResourceManager.Common; +using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.WindowsAzure.Commands.Common.CustomAttributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + [CmdletPreview("The cmdlet group \"AzConfig\" is in preview. Feedback is welcome: https://github.com/Azure/azure-powershell/discussions")] + public abstract class ConfigCommandBase : AzureRMCmdlet + { + private readonly RuntimeDefinedParameterDictionary _dynamicParameters = new RuntimeDefinedParameterDictionary(); + + protected IConfigManager ConfigManager { get; } + protected IEnumerable ConfigDefinitions + { + get + { + if (_configDefinitions == null) + { + _configDefinitions = ConfigManager.ListConfigDefinitions(); + } + return _configDefinitions; + } + } + private IEnumerable _configDefinitions; + + public ConfigCommandBase() : base() + { + if (!AzureSession.Instance.TryGetComponent(nameof(IConfigManager), out var configManager)) + { + throw new AzPSApplicationException($"Unexpected error: {nameof(IConfigManager)} has not been registered to the current session."); + } + ConfigManager = configManager; + } + + [Parameter(HelpMessage = "Specifies what part of Azure PowerShell the config applies to. Possible values are:\n- \"" + ConfigFilter.GlobalAppliesTo + "\": the config applies to all modules and cmdlets of Azure PowerShell. \n- Module name: the config applies to a certain module of Azure PowerShell. For example, \"Az.Storage\".\n- Cmdlet name: the config applies to a certain cmdlet of Azure PowerShell. For example, \"Get-AzKeyVault\".\nIf not specified, when getting configs, output will be all of the above; when updating, it defaults to \"" + ConfigFilter.GlobalAppliesTo + "\"; when clearing, configs applying to any targets are cleared.")] + [ValidateNotNullOrEmpty] + public string AppliesTo { get; set; } + + [Parameter(HelpMessage = "Determines the scope of config changes, for example, whether changes apply only to the current process, or to all sessions started by this user. By default it is CurrentUser.")] + public ConfigScope Scope { get; set; } = ConfigScope.CurrentUser; + + protected override void BeginProcessing() + { + base.BeginProcessing(); + ValidateParameters(); + } + + protected virtual void ValidateParameters() + { + if (!AppliesToHelper.TryParseAppliesTo(AppliesTo, out _)) + { + throw new AzPSArgumentException($"{nameof(AppliesTo)} must be a valid module name, a cmdlet name, or \"{ConfigFilter.GlobalAppliesTo}\"", nameof(AppliesTo)); + } + } + + protected object GetDynamicParameters(Func mapConfigToParameter) + { + _dynamicParameters.Clear(); + foreach (var config in ConfigDefinitions) + { + _dynamicParameters.Add(config.Key, mapConfigToParameter(config)); + } + return _dynamicParameters; + } + + /// + /// Gets the dynamic parameters and their values if specified. + /// + /// + protected IEnumerable<(string Key, object Value)> GetConfigsSpecifiedByUser() + { + var configs = new Dictionary(); + foreach (var param in _dynamicParameters.Values.Where(p => p.IsSet)) + { + yield return (param.Name, param.Value); + } + } + } +} diff --git a/src/Accounts/Accounts/Config/GetConfigCommand.cs b/src/Accounts/Accounts/Config/GetConfigCommand.cs new file mode 100644 index 000000000000..9f0147e88e1d --- /dev/null +++ b/src/Accounts/Accounts/Config/GetConfigCommand.cs @@ -0,0 +1,66 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.ResourceManager.Common; +using Microsoft.Azure.PowerShell.Common.Config; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Management.Automation; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + [Cmdlet(VerbsCommon.Get, AzureRMConstants.AzureRMPrefix + "Config")] + [OutputType(typeof(PSConfig))] + public class GetConfigCommand : ConfigCommandBase, IDynamicParameters + { + public GetConfigCommand() : base() + { + } + + public new object GetDynamicParameters() + { + return GetDynamicParameters((ConfigDefinition config) => + new RuntimeDefinedParameter( + config.Key, + typeof(SwitchParameter), + new Collection() { + new ParameterAttribute { + HelpMessage = config.HelpMessage + } + })); + } + + public override void ExecuteCmdlet() + { + ConfigFilter filter = CreateConfigFilter(); + + IEnumerable configs = ConfigManager.ListConfigs(filter); + WriteObject(configs.Select(x => new PSConfig(x)), true); + } + + private ConfigFilter CreateConfigFilter() + { + ConfigFilter filter = new ConfigFilter() { AppliesTo = AppliesTo }; + IEnumerable configKeysFromInput = GetConfigsSpecifiedByUser().Where(x => (bool)x.Value).Select(x => x.Key); + if (configKeysFromInput.Any()) + { + filter.Keys = configKeysFromInput; + } + + return filter; + } + } +} diff --git a/src/Accounts/Accounts/Config/UpdateConfigCommand.cs b/src/Accounts/Accounts/Config/UpdateConfigCommand.cs new file mode 100644 index 000000000000..83fb44f9801d --- /dev/null +++ b/src/Accounts/Accounts/Config/UpdateConfigCommand.cs @@ -0,0 +1,75 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.PowerShell.Common.Config; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Management.Automation; + +namespace Microsoft.Azure.Commands.Common.Authentication.Config +{ + [Cmdlet("Update", "AzConfig", SupportsShouldProcess = true)] + [OutputType(typeof(PSConfig))] + public class UpdateConfigCommand : ConfigCommandBase, IDynamicParameters + { + private const string ProcessMessage = "Update the configs that apply to \"{0}\" by the following keys: {1}."; + private string ProcessTarget => $"Configs in scope {Scope}"; + + public new object GetDynamicParameters() => GetDynamicParameters( + (ConfigDefinition config) => new RuntimeDefinedParameter( + config.Key, config.ValueType, + new Collection() { new ParameterAttribute { + HelpMessage = config.HelpMessage, + ValueFromPipelineByPropertyName = true + } } + )); + + protected override void BeginProcessing() + { + base.BeginProcessing(); + if (AppliesTo == null) + { + AppliesTo = ConfigFilter.GlobalAppliesTo; + } + } + + public override void ExecuteCmdlet() + { + var configsFromInput = GetConfigsSpecifiedByUser(); + if (!configsFromInput.Any()) + { + WriteWarning($"Please specify the key(s) of the configs to update. Run `help {MyInvocation.MyCommand.Name}` for more information."); + return; + } + base.ConfirmAction( + string.Format(ProcessMessage, AppliesTo, string.Join(", ", configsFromInput.Select(x => x.Key))), + ProcessTarget, + () => UpdateConfigs(configsFromInput)); + } + + private void UpdateConfigs(IEnumerable<(string, object)> configsToUpdate) + { + foreach ((string key, object value) in configsToUpdate) + { + ConfigData updated = ConfigManager.UpdateConfig(new UpdateConfigOptions(key, value, Scope) + { + AppliesTo = AppliesTo + }); + WriteObject(new PSConfig(updated)); + } + } + } +} diff --git a/src/Accounts/Accounts/help/Az.Accounts.md b/src/Accounts/Accounts/help/Az.Accounts.md index c05e40b851cb..351244e89071 100644 --- a/src/Accounts/Accounts/help/Az.Accounts.md +++ b/src/Accounts/Accounts/help/Az.Accounts.md @@ -14,6 +14,9 @@ Manages credentials and common configuration for all Azure modules. ### [Add-AzEnvironment](Add-AzEnvironment.md) Adds endpoints and metadata for an instance of Azure Resource Manager. +### [Clear-AzConfig](Clear-AzConfig.md) +Clears the values of configs that are set by the user. + ### [Clear-AzContext](Clear-AzContext.md) Remove all Azure credentials, account, and subscription information. @@ -56,6 +59,9 @@ Enables AzureRm prefix aliases for Az modules. ### [Get-AzAccessToken](Get-AzAccessToken.md) Get raw access token. When using -ResourceUrl, please make sure the value does match current Azure environment. You may refer to the value of `(Get-AzContext).Environment`. +### [Get-AzConfig](Get-AzConfig.md) +Gets the configs of Azure PowerShell. + ### [Get-AzContext](Get-AzContext.md) Gets the metadata used to authenticate Azure Resource Manager requests. @@ -120,3 +126,6 @@ Sets properties for an Azure environment. ### [Uninstall-AzureRm](Uninstall-AzureRm.md) Removes all AzureRm modules from a machine. +### [Update-AzConfig](Update-AzConfig.md) +Updates the configs of Azure PowerShell. + diff --git a/src/Accounts/Accounts/help/Clear-AzConfig.md b/src/Accounts/Accounts/help/Clear-AzConfig.md new file mode 100644 index 000000000000..55ea44291f4c --- /dev/null +++ b/src/Accounts/Accounts/help/Clear-AzConfig.md @@ -0,0 +1,251 @@ +--- +external help file: Microsoft.Azure.PowerShell.Cmdlets.Accounts.dll-Help.xml +Module Name: Az.Accounts +online version: https://docs.microsoft.com/powershell/module/az.accounts/clear-azconfig +schema: 2.0.0 +--- + +# Clear-AzConfig + +## SYNOPSIS +Clears the values of configs that are set by the user. + +## SYNTAX + +### ClearAll +``` +Clear-AzConfig [-All] [-Force] [-PassThru] [-AppliesTo ] [-Scope ] + [-DefaultProfile ] [-WhatIf] [-Confirm] [] +``` + +### ClearByKey +``` +Clear-AzConfig [-PassThru] [-AppliesTo ] [-Scope ] + [-DefaultProfile ] [-WhatIf] [-Confirm] [-DefaultSubscriptionForLogin] + [-EnableDataCollection] [-EnableInterceptSurvey] [-SuppressWarningMessage] [] +``` + +## DESCRIPTION +{{ Fill in the Description }} + +## EXAMPLES + +### Example 1 +```powershell +Clear-AzConfig -Todo +``` + +```output +Todo +``` + +Todo + +## PARAMETERS + +### -All +Clear all configs. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: ClearAll +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -AppliesTo +Specifies what part of Azure PowerShell the config applies to. +Possible values are: +- "Az": the config applies to all modules and cmdlets of Azure PowerShell. +- Module name: the config applies to a certain module of Azure PowerShell. +For example, "Az.Storage". +- Cmdlet name: the config applies to a certain cmdlet of Azure PowerShell. +For example, "Get-AzKeyVault". +If not specified, when getting configs, output will be all of the above; when updating or clearing configs, it defaults to "Az" + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DefaultProfile +The credentials, account, tenant, and subscription used for communication with Azure. + +```yaml +Type: Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core.IAzureContextContainer +Parameter Sets: (All) +Aliases: AzContext, AzureRmContext, AzureCredential + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DefaultSubscriptionForLogin +Subscription name or GUID. +If defined, when logging in Azure PowerShell without specifying the subscription, this one will be used to select the default context. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: ClearByKey +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -EnableDataCollection +todo + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: ClearByKey +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -EnableInterceptSurvey +When enabled, a message of taking part in the survey about the user experience of Azure PowerShell will prompt at low frequency. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: ClearByKey +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force +Do not ask for confirmation when clearing all configs. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: ClearAll +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PassThru +Returns true if cmdlet executes correctly. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Scope +Determines the scope of config changes, for example, whether changes apply only to the current process, or to all sessions started by this user. +By default it is CurrentUser. + +```yaml +Type: Microsoft.Azure.PowerShell.Common.Config.ConfigScope +Parameter Sets: (All) +Aliases: +Accepted values: CurrentUser, Process, Default + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -SuppressWarningMessage +Controls if the warning messages of upcoming breaking changes are enabled or suppressed. +The messages are typically displayed when a cmdlet that will have breaking change in the future is executed. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: ClearByKey +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Confirm +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhatIf +Shows what would happen if the cmdlet runs. +The cmdlet is not run. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + +## OUTPUTS + +### System.Boolean + +## NOTES + +## RELATED LINKS diff --git a/src/Accounts/Accounts/help/Get-AzConfig.md b/src/Accounts/Accounts/help/Get-AzConfig.md new file mode 100644 index 000000000000..f51d073b6ea4 --- /dev/null +++ b/src/Accounts/Accounts/help/Get-AzConfig.md @@ -0,0 +1,168 @@ +--- +external help file: Microsoft.Azure.PowerShell.Cmdlets.Accounts.dll-Help.xml +Module Name: Az.Accounts +online version: https://docs.microsoft.com/powershell/module/az.accounts/get-azconfig +schema: 2.0.0 +--- + +# Get-AzConfig + +## SYNOPSIS +Gets the configs of Azure PowerShell. + +## SYNTAX + +``` +Get-AzConfig [-AppliesTo ] [-Scope ] [-DefaultProfile ] + [-DefaultSubscriptionForLogin] [-EnableDataCollection] [-EnableInterceptSurvey] [-SuppressWarningMessage] + [] +``` + +## DESCRIPTION +{{ Fill in the Description }} + +## EXAMPLES + +### Example 1 +```powershell +Get-AzConfig +``` + +```output +Todo +``` + +Todo + +## PARAMETERS + +### -AppliesTo +Specifies what part of Azure PowerShell the config applies to. +Possible values are: +- "Az": the config applies to all modules and cmdlets of Azure PowerShell. +- Module name: the config applies to a certain module of Azure PowerShell. +For example, "Az.Storage". +- Cmdlet name: the config applies to a certain cmdlet of Azure PowerShell. +For example, "Get-AzKeyVault". +If not specified, when getting configs, output will be all of the above; when updating or clearing configs, it defaults to "Az" + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DefaultProfile +The credentials, account, tenant, and subscription used for communication with Azure. + +```yaml +Type: Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core.IAzureContextContainer +Parameter Sets: (All) +Aliases: AzContext, AzureRmContext, AzureCredential + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DefaultSubscriptionForLogin +Subscription name or GUID. +If defined, when logging in Azure PowerShell without specifying the subscription, this one will be used to select the default context. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -EnableDataCollection +todo + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -EnableInterceptSurvey +When enabled, a message of taking part in the survey about the user experience of Azure PowerShell will prompt at low frequency. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Scope +Determines the scope of config changes, for example, whether changes apply only to the current process, or to all sessions started by this user. +By default it is CurrentUser. + +```yaml +Type: Microsoft.Azure.PowerShell.Common.Config.ConfigScope +Parameter Sets: (All) +Aliases: +Accepted values: CurrentUser, Process, Default + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -SuppressWarningMessage +Controls if the warning messages of upcoming breaking changes are enabled or suppressed. +The messages are typically displayed when a cmdlet that will have breaking change in the future is executed. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + +## OUTPUTS + +### Microsoft.Azure.Commands.Common.Authentication.Config.PSConfig + +## NOTES + +## RELATED LINKS diff --git a/src/Accounts/Accounts/help/Update-AzConfig.md b/src/Accounts/Accounts/help/Update-AzConfig.md new file mode 100644 index 000000000000..a67982f7051f --- /dev/null +++ b/src/Accounts/Accounts/help/Update-AzConfig.md @@ -0,0 +1,199 @@ +--- +external help file: Microsoft.Azure.PowerShell.Cmdlets.Accounts.dll-Help.xml +Module Name: Az.Accounts +online version: https://docs.microsoft.com/powershell/module/az.accounts/update-azconfig +schema: 2.0.0 +--- + +# Update-AzConfig + +## SYNOPSIS +Updates the configs of Azure PowerShell. + +## SYNTAX + +``` +Update-AzConfig [-AppliesTo ] [-Scope ] [-DefaultProfile ] + [-WhatIf] [-Confirm] [-DefaultSubscriptionForLogin ] [-EnableDataCollection ] + [-EnableInterceptSurvey ] [-SuppressWarningMessage ] [] +``` + +## DESCRIPTION +{{ Fill in the Description }} + +## EXAMPLES + +### Example 1 +```powershell +Update-AzConfig -Todo $true +``` + +```output +Todo +``` + +Todo + +## PARAMETERS + +### -AppliesTo +Specifies what part of Azure PowerShell the config applies to. +Possible values are: +- "Az": the config applies to all modules and cmdlets of Azure PowerShell. +- Module name: the config applies to a certain module of Azure PowerShell. +For example, "Az.Storage". +- Cmdlet name: the config applies to a certain cmdlet of Azure PowerShell. +For example, "Get-AzKeyVault". +If not specified, when getting configs, output will be all of the above; when updating or clearing configs, it defaults to "Az" + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DefaultProfile +The credentials, account, tenant, and subscription used for communication with Azure. + +```yaml +Type: Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core.IAzureContextContainer +Parameter Sets: (All) +Aliases: AzContext, AzureRmContext, AzureCredential + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DefaultSubscriptionForLogin +Subscription name or GUID. +If defined, when logging in Azure PowerShell without specifying the subscription, this one will be used to select the default context. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -EnableDataCollection +todo + +```yaml +Type: System.Boolean +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -EnableInterceptSurvey +When enabled, a message of taking part in the survey about the user experience of Azure PowerShell will prompt at low frequency. + +```yaml +Type: System.Boolean +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Scope +Determines the scope of config changes, for example, whether changes apply only to the current process, or to all sessions started by this user. +By default it is CurrentUser. + +```yaml +Type: Microsoft.Azure.PowerShell.Common.Config.ConfigScope +Parameter Sets: (All) +Aliases: +Accepted values: CurrentUser, Process, Default + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -SuppressWarningMessage +Controls if the warning messages of upcoming breaking changes are enabled or suppressed. +The messages are typically displayed when a cmdlet that will have breaking change in the future is executed. + +```yaml +Type: System.Boolean +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Confirm +Prompts you for confirmation before running the cmdlet. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: cf + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -WhatIf +Shows what would happen if the cmdlet runs. +The cmdlet is not run. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: wi + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + +## OUTPUTS + +### Microsoft.Azure.Commands.Common.Authentication.Config.PSConfig + +## NOTES + +## RELATED LINKS From c1e967f8c7c465c6215031faea471824c1bd8ec3 Mon Sep 17 00:00:00 2001 From: Yeming Liu <11371776+isra-fel@users.noreply.github.com> Date: Thu, 24 Mar 2022 16:41:24 +0800 Subject: [PATCH 4/9] test related --- .../Accounts.Test/Mocks/MockDataStore.cs | 3 +- src/Accounts/Accounts/Directory.Build.targets | 5 +- .../ConfigTests/ClearConfigTests.cs | 214 +++++++++++ .../ConfigTests/ConfigDefinitionTests.cs | 60 ++++ .../ConfigTests/ConfigTestsBase.cs | 75 ++++ .../ConfigTests/GetConfigTests.cs | 339 ++++++++++++++++++ .../ConfigTests/PriorityTests.cs | 85 +++++ .../ConfigTests/RegisterConfigTests.cs | 106 ++++++ .../ConfigTests/UpdateConfigTests.cs | 186 ++++++++++ .../Mocks/MockDataStore.cs | 3 +- .../Mocks/MockEnvironmentVariableProvider.cs | 53 +++ .../Authentication/Properties/AssemblyInfo.cs | 3 + 12 files changed, 1128 insertions(+), 4 deletions(-) create mode 100644 src/Accounts/Authentication.Test/ConfigTests/ClearConfigTests.cs create mode 100644 src/Accounts/Authentication.Test/ConfigTests/ConfigDefinitionTests.cs create mode 100644 src/Accounts/Authentication.Test/ConfigTests/ConfigTestsBase.cs create mode 100644 src/Accounts/Authentication.Test/ConfigTests/GetConfigTests.cs create mode 100644 src/Accounts/Authentication.Test/ConfigTests/PriorityTests.cs create mode 100644 src/Accounts/Authentication.Test/ConfigTests/RegisterConfigTests.cs create mode 100644 src/Accounts/Authentication.Test/ConfigTests/UpdateConfigTests.cs create mode 100644 src/Accounts/Authentication.Test/Mocks/MockEnvironmentVariableProvider.cs diff --git a/src/Accounts/Accounts.Test/Mocks/MockDataStore.cs b/src/Accounts/Accounts.Test/Mocks/MockDataStore.cs index 5f03997356c5..cfed5e22eb64 100644 --- a/src/Accounts/Accounts.Test/Mocks/MockDataStore.cs +++ b/src/Accounts/Accounts.Test/Mocks/MockDataStore.cs @@ -382,7 +382,8 @@ public Stream OpenForExclusiveWrite(string path) () => { writeLocks[path] = false; - virtualStore[path] = Encoding.UTF8.GetString(buffer); + // trim \0 otherwise json fails to parse + virtualStore[path] = Encoding.UTF8.GetString(buffer).TrimEnd('\0'); } ); } diff --git a/src/Accounts/Accounts/Directory.Build.targets b/src/Accounts/Accounts/Directory.Build.targets index fa3748d4339a..278dd06f9a08 100644 --- a/src/Accounts/Accounts/Directory.Build.targets +++ b/src/Accounts/Accounts/Directory.Build.targets @@ -1,9 +1,10 @@ - + + - + diff --git a/src/Accounts/Authentication.Test/ConfigTests/ClearConfigTests.cs b/src/Accounts/Authentication.Test/ConfigTests/ClearConfigTests.cs new file mode 100644 index 000000000000..b9fbba072cf6 --- /dev/null +++ b/src/Accounts/Authentication.Test/ConfigTests/ClearConfigTests.cs @@ -0,0 +1,214 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Exceptions; +using Microsoft.Azure.Commands.Common.Authentication.Config; +using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.Rest.ClientRuntime.Azure.TestFramework; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Xunit; + +namespace Microsoft.Azure.Authentication.Test.Config +{ + public class ClearConfigTests : ConfigTestsBase + { + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanClearSingleConfig() + { + string key = "FalseByDefault"; + IConfigManager icm = GetConfigManager(new SimpleTypedConfig(key, "{help message}", false)); + Assert.False(icm.GetConfigValue(key)); + + icm.UpdateConfig(new UpdateConfigOptions(key, true, ConfigScope.Process)); + Assert.True(icm.GetConfigValue(key)); + + icm.ClearConfig(new ClearConfigOptions(key, ConfigScope.Process)); + Assert.False(icm.GetConfigValue(key)); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CannotClearUnknownConfig() + { + IConfigManager configurationManager = GetConfigManager(); + + Assert.Throws(() => + { + configurationManager.ClearConfig(new ClearConfigOptions("NeverRegistered", ConfigScope.CurrentUser)); + }); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void ShouldNotThrowToClearConfigNeverSet() + { + string key1 = "key1"; + var config1 = new SimpleTypedConfig(key1, "{help message}", false); + string key2 = "key2"; + var config2 = new SimpleTypedConfig(key2, "{help message}", false); + IConfigManager icm = GetConfigManager(config1, config2); + + icm.ClearConfig(key1, ConfigScope.CurrentUser); + icm.ClearConfig(key2, ConfigScope.Process); + icm.ClearConfig(null, ConfigScope.CurrentUser); + icm.ClearConfig(null, ConfigScope.Process); + icm.ClearConfig(new ClearConfigOptions(null, ConfigScope.CurrentUser) + { + AppliesTo = null + }); + icm.ClearConfig(new ClearConfigOptions(null, ConfigScope.Process) + { + AppliesTo = null + }); + icm.ClearConfig(new ClearConfigOptions(null, ConfigScope.CurrentUser) + { + AppliesTo = "Az.Accounts" + }); + icm.ClearConfig(new ClearConfigOptions(null, ConfigScope.Process) + { + AppliesTo = "Az.Accounts" + }); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanClearSingleConfigInJson() + { + IConfigManager icm = GetConfigManager(); + string key = "DisableSomething"; + icm.RegisterConfig(new SimpleTypedConfig(key, "{help message}", false)); + icm.BuildConfig(); + + Assert.False(icm.GetConfigValue(key)); + + icm.UpdateConfig(new UpdateConfigOptions(key, true, ConfigScope.CurrentUser)); + Assert.True(icm.GetConfigValue(key)); + + icm.ClearConfig(new ClearConfigOptions(key, ConfigScope.CurrentUser)); + Assert.False(icm.GetConfigValue(key)); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanClearAllConfigsInJson() + { + string key1 = "key1"; + var config1 = new SimpleTypedConfig(key1, "{help message}", false); + string key2 = "key2"; + var config2 = new SimpleTypedConfig(key2, "{help message}", false); + ConfigManager cm = GetConfigManager(config1, config2) as ConfigManager; + + Assert.False(cm.GetConfigValue(key1)); + Assert.False(cm.GetConfigValue(key2)); + + // Scenario 1: update the configs, applying to Az + cm.UpdateConfig(new UpdateConfigOptions(key1, true, ConfigScope.CurrentUser)); + Assert.True(cm.GetConfigValue(key1)); + cm.UpdateConfig(new UpdateConfigOptions(key2, true, ConfigScope.CurrentUser)); + Assert.True(cm.GetConfigValue(key2)); + + // clear all configs by specifying `null` as the key, applying to Az + cm.ClearConfig(null, ConfigScope.CurrentUser); + Assert.False(cm.GetConfigValue(key1)); + Assert.False(cm.GetConfigValue(key2)); + + // Scenario 2: update the configs, applying to Az.Accounts + cm.UpdateConfig(new UpdateConfigOptions(key1, true, ConfigScope.CurrentUser) { AppliesTo = "Az.Accounts" }); + Assert.True(cm.GetConfigValueInternal(key1, new InternalInvocationInfo() { ModuleName = "Az.Accounts" })); + cm.UpdateConfig(new UpdateConfigOptions(key2, true, ConfigScope.CurrentUser) { AppliesTo = "Az.Accounts" }); + Assert.True(cm.GetConfigValueInternal(key2, new InternalInvocationInfo() { ModuleName = "Az.Accounts" })); + + // clear all configs, applying to Az.Accounts + cm.ClearConfig(new ClearConfigOptions(null, ConfigScope.CurrentUser) { AppliesTo = "Az.Accounts" }); + Assert.False(cm.GetConfigValueInternal(key1, new InternalInvocationInfo() { ModuleName = "Az.Accounts" })); + Assert.False(cm.GetConfigValueInternal(key2, new InternalInvocationInfo() { ModuleName = "Az.Accounts" })); + + // Scenario 3: update the configs, applying differently + cm.UpdateConfig(new UpdateConfigOptions(key1, true, ConfigScope.CurrentUser) { AppliesTo = "Az.Accounts" }); + Assert.True(cm.GetConfigValueInternal(key1, new InternalInvocationInfo() { ModuleName = "Az.Accounts" })); + cm.UpdateConfig(new UpdateConfigOptions(key2, true, ConfigScope.CurrentUser) { AppliesTo = "Az.KeyVault" }); + Assert.True(cm.GetConfigValueInternal(key2, new InternalInvocationInfo() { ModuleName = "Az.KeyVault" })); + + // clear all configs, applying anything + cm.ClearConfig(null, ConfigScope.CurrentUser); + Assert.False(cm.GetConfigValueInternal(key1, new InternalInvocationInfo() { ModuleName = "Az.Accounts" })); + Assert.False(cm.GetConfigValueInternal(key2, new InternalInvocationInfo() { ModuleName = "Az.KeyVault" })); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void ShouldNotThrowWhenClearConfigNeverSet() + { + string key = "DisableSomething"; + var config = new SimpleTypedConfig(key, "{help message}", false); + IConfigManager icm = GetConfigManager(config); + + icm.ClearConfig(key, ConfigScope.CurrentUser); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanClearByScope() + { + const string boolKey = "BoolKey"; + var boolConfig = new SimpleTypedConfig(boolKey, "", false); + const string intKey = "intKey"; + var intConfig = new SimpleTypedConfig(intKey, "", 0); + var icm = GetConfigManager(boolConfig, intConfig); + + icm.UpdateConfig(new UpdateConfigOptions(boolKey, true, ConfigScope.CurrentUser)); + icm.UpdateConfig(new UpdateConfigOptions(intKey, 10, ConfigScope.CurrentUser)); + icm.UpdateConfig(new UpdateConfigOptions(boolKey, true, ConfigScope.Process)); + icm.UpdateConfig(new UpdateConfigOptions(intKey, 10, ConfigScope.Process)); + + icm.ClearConfig(new ClearConfigOptions(boolKey, ConfigScope.Process)); + icm.ClearConfig(new ClearConfigOptions(intKey, ConfigScope.Process)); + + foreach (var configData in icm.ListConfigs()) + { + Assert.NotEqual(ConfigScope.Process, configData.Scope); + } + + icm.ClearConfig(boolKey, ConfigScope.CurrentUser); + icm.ClearConfig(intKey, ConfigScope.CurrentUser); + + foreach (var configData in icm.ListConfigs()) + { + Assert.NotEqual(ConfigScope.CurrentUser, configData.Scope); + } + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void AppliesToShouldDefaultToAz() + { + const string boolKey = "BoolKey"; + var boolConfig = new SimpleTypedConfig(boolKey, "", false); + var icm = GetConfigManager(boolConfig); + + const string appliesTo = "Az.A"; + icm.UpdateConfig(new UpdateConfigOptions(boolKey, true, ConfigScope.CurrentUser) + { + AppliesTo = appliesTo + }); + + icm.ClearConfig(boolKey, ConfigScope.CurrentUser); + Assert.Single(icm.ListConfigs(new ConfigFilter() { Keys = new string[] { boolKey }, AppliesTo = appliesTo })); + + icm.ClearConfig(new ClearConfigOptions(boolKey, ConfigScope.CurrentUser) { AppliesTo = appliesTo }); + Assert.Empty(icm.ListConfigs(new ConfigFilter() { Keys = new string[] { boolKey }, AppliesTo = appliesTo })); + } + } +} diff --git a/src/Accounts/Authentication.Test/ConfigTests/ConfigDefinitionTests.cs b/src/Accounts/Authentication.Test/ConfigTests/ConfigDefinitionTests.cs new file mode 100644 index 000000000000..b451c1bea556 --- /dev/null +++ b/src/Accounts/Authentication.Test/ConfigTests/ConfigDefinitionTests.cs @@ -0,0 +1,60 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Exceptions; +using Microsoft.Azure.Commands.Common.Authentication.Config; +using Microsoft.Rest.ClientRuntime.Azure.TestFramework; +using Microsoft.WindowsAzure.Commands.Common; +using System; +using System.Linq; +using Xunit; +using Microsoft.Azure.PowerShell.Common.Config; + +namespace Microsoft.Azure.Authentication.Test.Config +{ + public class ConfigDefinitionTests : ConfigTestsBase + { + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanValidateInput() { + const string boolKey = "BoolKey"; + var boolConfig = new SimpleTypedConfig(boolKey, "", false); + var rangedIntConfig = new RangedConfig(); + var icm = GetConfigManagerWithInitState(null, null, boolConfig, rangedIntConfig); + + Assert.Throws(() => { icm.UpdateConfig(boolKey, 0, ConfigScope.CurrentUser); }); + Assert.Throws(() => { icm.UpdateConfig(rangedIntConfig.Key, true, ConfigScope.CurrentUser); }); + Assert.Throws(() => { icm.UpdateConfig(rangedIntConfig.Key, -1, ConfigScope.CurrentUser); }); + } + + private class RangedConfig : TypedConfig + { + public override object DefaultValue => 0; + + public override string Key => "RangedKey"; + + public override string HelpMessage => ""; + + public override void Validate(object value) + { + base.Validate(value); + int valueAsInt = (int)value; + if (valueAsInt < 0 || valueAsInt > 100) + { + throw new ArgumentOutOfRangeException($"The value of config {Key} must be in between 0 and 100."); + } + } + } + } +} diff --git a/src/Accounts/Authentication.Test/ConfigTests/ConfigTestsBase.cs b/src/Accounts/Authentication.Test/ConfigTests/ConfigTestsBase.cs new file mode 100644 index 000000000000..771641cee939 --- /dev/null +++ b/src/Accounts/Authentication.Test/ConfigTests/ConfigTestsBase.cs @@ -0,0 +1,75 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config; +using Microsoft.Azure.PowerShell.Authentication.Test.Mocks; +using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.WindowsAzure.Commands.Common.Test.Mocks; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.Azure.Authentication.Test.Config +{ + public class ConfigTestsBase + { + private readonly Action _noopFileWriter = (x, y) => { }; + private readonly Action _noopEnvVarWriter = (x) => { }; + + /// + /// Initializes and returns an with the specified configs registered. + /// + /// Definitions of configs to be registered to the config manager. + /// A config manager ready to use. + protected IConfigManager GetConfigManager(params ConfigDefinition[] config) => GetConfigManagerWithInitState(null, null, config); + + /// + /// Initializes and returns an with the specified configs registered with initial state. + /// + /// An action to set up the config file before config manager initializes. + /// An action to set up the environments before config manager initializes. + /// Definitions of configs to be registered to the config manager. + /// A config manager with initial state, ready to use. + protected IConfigManager GetConfigManagerWithInitState(Action configFileWriter, Action envVarWriter, params ConfigDefinition[] config) + { + if (configFileWriter == null) + { + configFileWriter = _noopFileWriter; + } + + if (envVarWriter == null) + { + envVarWriter = _noopEnvVarWriter; + } + + string configPath = Path.GetRandomFileName(); + var mockDataStore = new MockDataStore(); + configFileWriter(mockDataStore, configPath); + var environmentVariables = new MockEnvironmentVariableProvider(); + envVarWriter(environmentVariables); + ConfigInitializer ci = new ConfigInitializer(new List() { configPath }) + { + DataStore = mockDataStore, + EnvironmentVariableProvider = environmentVariables + }; + IConfigManager icm = ci.GetConfigManager(); + foreach (var configDefinition in config) + { + icm.RegisterConfig(configDefinition); + } + icm.BuildConfig(); + return icm; + } + } +} diff --git a/src/Accounts/Authentication.Test/ConfigTests/GetConfigTests.cs b/src/Accounts/Authentication.Test/ConfigTests/GetConfigTests.cs new file mode 100644 index 000000000000..a1366a550dac --- /dev/null +++ b/src/Accounts/Authentication.Test/ConfigTests/GetConfigTests.cs @@ -0,0 +1,339 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Exceptions; +using Microsoft.Azure.Commands.Common.Authentication.Config; +using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.Rest.ClientRuntime.Azure.TestFramework; +using System.Linq; +using Xunit; + +namespace Microsoft.Azure.Authentication.Test.Config +{ + public class GetConfigTests : ConfigTestsBase + { + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanGetAppliesTo() + { + const string key = "EnableTelemetry"; + var def = new SimpleTypedConfig( + key, + "Enable telemetry", + true); + IConfigManager icm = GetConfigManager(def); + + var config = icm.ListConfigs().Single(); + Assert.NotNull(config); + Assert.Equal(key, config.Definition.Key); + + icm.UpdateConfig(new UpdateConfigOptions(key, false, ConfigScope.CurrentUser)); + config = icm.ListConfigs().Single(); + Assert.Equal(ConfigFilter.GlobalAppliesTo, config.AppliesTo); + + icm.UpdateConfig(new UpdateConfigOptions(key, false, ConfigScope.CurrentUser) { AppliesTo = "Az.KeyVault" }); + config = icm.ListConfigs(new ConfigFilter() { AppliesTo = "Az.KeyVault" }).Single(); + Assert.Equal("Az.KeyVault", config.AppliesTo); + + icm.UpdateConfig(new UpdateConfigOptions(key, false, ConfigScope.CurrentUser) { AppliesTo = "Get-AzKeyVault" }); + config = icm.ListConfigs(new ConfigFilter { AppliesTo = "Get-AzKeyVault" }).Single(); + Assert.Equal("Get-AzKeyVault", config.AppliesTo); + + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void ShouldReturnEmptyWhenFilterIsWrong() + { + const string key = "EnableTelemetry"; + var config = new SimpleTypedConfig( + key, + "Enable telemetry", + true); + var icm = GetConfigManager(config); + Assert.NotEmpty(icm.ListConfigs()); + Assert.NotEmpty(icm.ListConfigs(null)); + Assert.Empty(icm.ListConfigs(new ConfigFilter() { Keys = new string[] { "Never Exist" } })); + Assert.Empty(icm.ListConfigs(new ConfigFilter() { AppliesTo = "xxx" })); + + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanGetAndListRegisteredConfigs() + { + const string key1 = "EnableTelemetry"; + var config1 = new SimpleTypedConfig( + key1, + "Enable telemetry", + true); + TestConfig config2 = new TestConfig(); + IConfigManager configurationManager = GetConfigManager(config1, config2); + + var listResult = configurationManager.ListConfigs(); + Assert.Equal(2, listResult.Count()); + + ConfigData configData = listResult.Where(x => x.Definition.Key == key1).Single(); + Assert.Equal(true, configData.Value); + Assert.True(configurationManager.GetConfigValue(key1)); + + ConfigData tempConfigResult = listResult.Where(x => x.Definition.Key == config2.Key).Single(); + Assert.Equal(config2.DefaultValue, tempConfigResult.Value); + Assert.Equal(config2.HelpMessage, tempConfigResult.Definition.HelpMessage); + Assert.Equal(config2.DefaultValue, configurationManager.GetConfigValue(config2.Key)); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanUpdateAndList() + { + IConfigManager configurationManager = GetConfigManager(); + const string key = "EnableTelemetry"; + configurationManager.RegisterConfig( + new SimpleTypedConfig( + key, + "Enable telemetry", + true)); + configurationManager.BuildConfig(); + var updatedConfig = configurationManager.UpdateConfig(new UpdateConfigOptions(key, false, ConfigScope.Process)); + Assert.Equal(key, updatedConfig.Definition.Key); + Assert.False((bool)updatedConfig.Value); + Assert.False(configurationManager.GetConfigValue(key)); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanGetFromEnvironmentVar() + { + const string key = "FromEnv"; + const string envKey = "ENV_VAR_FOR_CONFIG"; + var config = new SimpleTypedConfig(key, "", -1, envKey); + const int value = 20; + + var configurationManager = GetConfigManagerWithInitState(null, (envVar) => { envVar.Set(envKey, value.ToString()); }, config); + + Assert.Equal(value, configurationManager.GetConfigValue(key)); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void ShouldNotThrowWhenEnvVarIsWrong() + { + const string key = "FromEnv"; + const string envKey = "ENV_VAR_FOR_CONFIG"; + const int defaultValue = -1; + var config = new SimpleTypedConfig(key, "", defaultValue, envKey); + const bool valueWithWrongType = true; + var configurationManager = GetConfigManagerWithInitState(null, envVar => + { + envVar.Set(envKey, valueWithWrongType.ToString()); + }, config); + + Assert.Equal(defaultValue, configurationManager.GetConfigValue(key)); + } + + private class TestConfig : TypedConfig + { + public override object DefaultValue => -1; + + public override string Key => "TempConfig"; + + public override string HelpMessage => "temp config"; + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanGetFromJson() + { + var config1 = new SimpleTypedConfig("Retry", "", -1); + var config2 = new SimpleTypedConfig("Array", "", null); + IConfigManager icm = GetConfigManagerWithInitState((dataStore, path) => + { + dataStore.WriteFile(path, +@"{ + ""Az"": { + ""Retry"": 100 + }, + ""Az.KeyVault"": { + ""Array"": [""a"",""b""] + }, + ""Get-AzKeyVault"": { + ""Array"": [""k"",""v""] + } +}"); + }, null, config1, config2); + ConfigManager cm = icm as ConfigManager; + Assert.Equal(100, cm.GetConfigValue("Retry")); + Assert.Equal(new string[] { "a", "b" }, cm.GetConfigValueInternal("Array", new InternalInvocationInfo() { ModuleName = "Az.KeyVault" })); + Assert.Equal(new string[] { "k", "v" }, cm.GetConfigValueInternal("Array", new InternalInvocationInfo() { ModuleName = "Az.KeyVault", CmdletName = "Get-AzKeyVault" })); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void ThrowWhenGetUnknownConfig() + { + IConfigManager entry = GetConfigManager(); + entry.BuildConfig(); + + Assert.Throws(() => { entry.GetConfigValue(null); }); + Assert.Throws(() => { entry.GetConfigValue(""); }); + + const string key = "KeyThatIsNotRegistered"; + Assert.Throws(() => { entry.GetConfigValue(key); }); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanFilterByKeyAndAppliesTo() + { + const string key = "key"; + var config = new SimpleTypedConfig(key, "", true); + var icm = GetConfigManager(config); + const string module = "Az.KeyVault"; + icm.UpdateConfig(new UpdateConfigOptions(key, false, ConfigScope.CurrentUser) { AppliesTo = module }); + Assert.Single(icm.ListConfigs(new ConfigFilter() { Keys = new[] { key }, AppliesTo = module })); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanFilterByKey() + { + const string key = "key"; + var config = new SimpleTypedConfig(key, "", true); + var icm = GetConfigManager(config); + const string module = "Az.KeyVault"; + icm.UpdateConfig(new UpdateConfigOptions(key, false, ConfigScope.CurrentUser) { AppliesTo = module }); + var listResults = icm.ListConfigs(new ConfigFilter() { Keys = new[] { key } }); + Assert.Equal(2, listResults.Count()); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanFilterByAppliesTo() + { + const string key1 = "key"; + var config1 = new SimpleTypedConfig(key1, "", true); + const string key2 = "key2"; + var config2 = new SimpleTypedConfig(key2, "", true); + var icm = GetConfigManager(config1, config2); + + const string module = "Az.KeyVault"; + icm.UpdateConfig(new UpdateConfigOptions(key1, false, ConfigScope.CurrentUser) { AppliesTo = module }); + icm.UpdateConfig(new UpdateConfigOptions(key2, false, ConfigScope.CurrentUser) { AppliesTo = module }); + + var listResults = icm.ListConfigs(new ConfigFilter() { AppliesTo = module }); + Assert.Equal(2, listResults.Count()); + + listResults = icm.ListConfigs(new ConfigFilter() { AppliesTo = ConfigFilter.GlobalAppliesTo }); + Assert.Equal(2, listResults.Count()); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanFilterByNoFilter() + { + const string key1 = "key"; + var config1 = new SimpleTypedConfig(key1, "", true); + const string key2 = "key2"; + var config2 = new SimpleTypedConfig(key2, "", true); + var icm = GetConfigManager(config1, config2); + + const string module = "Az.KeyVault"; + icm.UpdateConfig(new UpdateConfigOptions(key1, false, ConfigScope.CurrentUser) { AppliesTo = module }); + icm.UpdateConfig(new UpdateConfigOptions(key2, false, ConfigScope.CurrentUser) { AppliesTo = module }); + var listResults = icm.ListConfigs(); + Assert.Equal(4, listResults.Count()); // default*2, module*2 + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanListDefinitions() + { + const string key1 = "key"; + var config1 = new SimpleTypedConfig(key1, "", true); + const string key2 = "key2"; + var config2 = new SimpleTypedConfig(key2, "", true); + var config3 = new TestConfig(); + var icm = GetConfigManager(config1, config2, config3); + + Assert.Equal(3, icm.ListConfigDefinitions().Count()); + + const string module = "Az.KeyVault"; + icm.UpdateConfig(new UpdateConfigOptions(key1, false, ConfigScope.CurrentUser) { AppliesTo = module }); + Assert.Equal(3, icm.ListConfigDefinitions().Count()); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanGetScope() + { + const string key1 = "key"; + var config1 = new SimpleTypedConfig(key1, "", true); + var config2 = new TestConfig(); + var icm = GetConfigManager(config1, config2); + + var listResults = icm.ListConfigs(); + foreach (var config in listResults) + { + Assert.Equal(ConfigScope.Default, config.Scope); + } + + var updated = icm.UpdateConfig(new UpdateConfigOptions(key1, false, ConfigScope.CurrentUser)); + Assert.Equal(ConfigScope.CurrentUser, updated.Scope); + + updated = icm.UpdateConfig(new UpdateConfigOptions(key1, true, ConfigScope.Process)); + Assert.Equal(ConfigScope.Process, updated.Scope); + + icm.ClearConfig(new ClearConfigOptions(key1, ConfigScope.Process)); + updated = icm.ListConfigs(new ConfigFilter() { Keys = new string[] { key1 } }).Single(); + Assert.Equal(ConfigScope.CurrentUser, updated.Scope); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void AppliesToShouldBeCaseInsensitive() + { + const string key = "key"; + var config = new SimpleTypedConfig(key, "", 0); + var icm = GetConfigManager(config); + + icm.UpdateConfig(new UpdateConfigOptions(key, 1, ConfigScope.CurrentUser) { AppliesTo = "az.abc" }); + Assert.Equal(1, icm.ListConfigs(new ConfigFilter() { Keys = new[] { key }, AppliesTo = "az.abc" }).Single().Value); + Assert.Equal(1, icm.ListConfigs(new ConfigFilter() { Keys = new[] { key }, AppliesTo = "Az.Abc" }).Single().Value); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void ListDefinitionsShouldBeDictOrder() + { + const string key1 = "key1"; + var config1 = new SimpleTypedConfig(key1, "", 0); + const string key2 = "key2"; + var config2 = new SimpleTypedConfig(key2, "", 0); + const string key3 = "key3"; + var config3 = new SimpleTypedConfig(key3, "", 0); + // register using wrong order + var icm = GetConfigManager(config2, config1, config3); + + for (int i = 0; i != 10; ++i) + { + var definitions = icm.ListConfigDefinitions(); + // expect return with dict order + Assert.Equal(key1, definitions.ElementAt(0).Key); + Assert.Equal(key2, definitions.ElementAt(1).Key); + Assert.Equal(key3, definitions.ElementAt(2).Key); + } + } + } +} diff --git a/src/Accounts/Authentication.Test/ConfigTests/PriorityTests.cs b/src/Accounts/Authentication.Test/ConfigTests/PriorityTests.cs new file mode 100644 index 000000000000..eb5972d0f571 --- /dev/null +++ b/src/Accounts/Authentication.Test/ConfigTests/PriorityTests.cs @@ -0,0 +1,85 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Config; +using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.Rest.ClientRuntime.Azure.TestFramework; +using Xunit; + +namespace Microsoft.Azure.Authentication.Test.Config +{ + public class PriorityTests : ConfigTestsBase + { + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void UserConfigHigherThanSystemUserEnv() + { + const string retryKey = "Retry"; + const string envName = "ENV_FOR_RETRY"; + var config = new SimpleTypedConfig(retryKey, "", -1, envName); + IConfigManager icm = GetConfigManagerWithInitState((dataStore, path) => + { + dataStore.WriteFile(path, +@"{ + ""Az"": { + ""Retry"": 100 + } +}"); + }, envVar => + { + envVar.Set(envName, "10", System.EnvironmentVariableTarget.User); + }, config); + Assert.Equal(100, icm.GetConfigValue(retryKey)); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void ProcessEnvHigherThanUserConfig() + { + + const string retryKey = "Retry"; + const string envName = "ENV_FOR_RETRY"; + var config = new SimpleTypedConfig(retryKey, "", -1, envName); + IConfigManager icm = GetConfigManagerWithInitState((dataStore, path) => + { + dataStore.WriteFile(path, +@"{ + ""Az"": { + ""Retry"": 100 + } +}"); + }, envVar => + { + envVar.Set(envName, "10", System.EnvironmentVariableTarget.Process); + }, config); + Assert.Equal(10, icm.GetConfigValue(retryKey)); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void ProcessConfigHigherThanProcessEnv() + { + const string retryKey = "Retry"; + const string envName = "ENV_FOR_RETRY"; + var config = new SimpleTypedConfig(retryKey, "", -1, envName); + IConfigManager icm = GetConfigManagerWithInitState(null, envVar => + { + envVar.Set(envName, "10", System.EnvironmentVariableTarget.Process); + }, config); + + icm.UpdateConfig(new UpdateConfigOptions(retryKey, 100, ConfigScope.Process)); + Assert.Equal(100, icm.GetConfigValue(retryKey)); + } + } +} diff --git a/src/Accounts/Authentication.Test/ConfigTests/RegisterConfigTests.cs b/src/Accounts/Authentication.Test/ConfigTests/RegisterConfigTests.cs new file mode 100644 index 000000000000..ec436c41afe5 --- /dev/null +++ b/src/Accounts/Authentication.Test/ConfigTests/RegisterConfigTests.cs @@ -0,0 +1,106 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Exceptions; +using Microsoft.Azure.Commands.Common.Authentication.Config; +using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.Rest.ClientRuntime.Azure.TestFramework; +using System; +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Azure.Authentication.Test.Config +{ + public class RegisterConfigTests : ConfigTestsBase + { + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CannotRegisterSameKeyTwice() + { + IConfigManager entry = GetConfigManager(); + const string key = "CannotRegisterTwice"; + entry.RegisterConfig(new SimpleTypedConfig(key, "", -1)); + Assert.Throws(() => + { + entry.RegisterConfig(new SimpleTypedConfig(key, "", null)); + }); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanRegisterSameConfigTwice() + { + IConfigManager entry = GetConfigManager(); + const string key = "CanRegisterTwice"; + SimpleTypedConfig config = new SimpleTypedConfig(key, "", -1); + entry.RegisterConfig(config); + entry.RegisterConfig(config); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanGetDefaultValue() + { + IConfigManager entry = GetConfigManager(); + const string key = "CanGetConfigValue"; + SimpleTypedConfig config = new SimpleTypedConfig(key, "", -1); + entry.RegisterConfig(config); + entry.BuildConfig(); + Assert.Equal(-1, entry.GetConfigValue(key)); + + entry.UpdateConfig(new UpdateConfigOptions(key, 10, ConfigScope.Process)); + Assert.Equal(10, entry.GetConfigValue(key)); + } + + [Theory] + [MemberData(nameof(TestData))] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanRegisterConfigs(ConfigDefinition config) + { + ConfigManager manager = GetConfigManager() as ConfigManager; + manager.RegisterConfig(config); + manager.BuildConfig(); + Assert.Equal(config.DefaultValue, manager.GetConfigValueInternal(config.Key, null)); + } + + public static IEnumerable TestData => new List + { + new object[] { new SimpleTypedConfig("Config", "", -1) }, + new object[] { new SimpleTypedConfig("Config", "", -1, "ENV_VAR_FOR_CONFIG") }, + new object[] { new SimpleTypedConfig("Config", "", null) }, + new object[] { new SimpleTypedConfig("Config", "", 1) }, + new object[] { new SimpleTypedConfig("Config", "", true) }, + new object[] { new SimpleTypedConfig("Config", "", "default") }, + new object[] { new SimpleTypedConfig("Config", "", 3.1415926) }, + new object[] { new SimpleTypedConfig("Config", "", new int[] { 1,2,3 }) }, + new object[] { new SimpleTypedConfig("Config", "", new string[] { "Az.Accounts", "Az.Compute" })}, + new object[] { new SimpleTypedConfig("Config", "", DateTime.MinValue) }, + new object[] { new SimpleTypedConfig("Config", "", true, "env_var", new [] { AppliesTo.Cmdlet }) }, + new object[] { new TestConfigForDefaultValue() } + }; + + private class TestConfigForDefaultValue : ConfigDefinition + { + public override object DefaultValue => (decimal)10; + + public override string Key => nameof(TestConfigForDefaultValue); + + public override string HelpMessage => ""; + + public override Type ValueType => typeof(decimal); + + public override void Validate(object value) { base.Validate(value); } + } + } +} diff --git a/src/Accounts/Authentication.Test/ConfigTests/UpdateConfigTests.cs b/src/Accounts/Authentication.Test/ConfigTests/UpdateConfigTests.cs new file mode 100644 index 000000000000..f7f6359d0d84 --- /dev/null +++ b/src/Accounts/Authentication.Test/ConfigTests/UpdateConfigTests.cs @@ -0,0 +1,186 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Exceptions; +using Microsoft.Azure.Commands.Common.Authentication.Config; +using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.Rest.ClientRuntime.Azure.TestFramework; +using Moq; +using System; +using System.Linq; +using Xunit; + +namespace Microsoft.Azure.Authentication.Test.Config +{ + public class UpdateConfigTests : ConfigTestsBase + { + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanUpdateJsonFile() + { + const string retryKey = "Retry"; + var intConfig = new SimpleTypedConfig(retryKey, "", -1); + const string arrayKey = "Array"; + var arrayConfig = new SimpleTypedConfig(arrayKey, "", null); + IConfigManager icm = GetConfigManagerWithInitState((dataStore, path) => + { + dataStore.WriteFile(path, +@"{ + ""Az"": { + ""Retry"": 100 + }, + ""Az.KeyVault"": { + ""Array"": [""a"",""b""] + } +}"); + }, null, intConfig, arrayConfig); + ConfigManager cm = icm as ConfigManager; + Assert.Equal(100, cm.GetConfigValue(retryKey)); + Assert.Equal(new string[] { "a", "b" }, cm.GetConfigValueInternal(arrayKey, new InternalInvocationInfo() { ModuleName = "Az.KeyVault" })); + ConfigData updated = icm.UpdateConfig(new UpdateConfigOptions(retryKey, 10, ConfigScope.CurrentUser)); + Assert.Equal(10, updated.Value); + Assert.Equal(10, icm.GetConfigValue(retryKey)); + + string[] updatedArray = new string[] { "c", "d" }; + ConfigData updated2 = icm.UpdateConfig(new UpdateConfigOptions(arrayKey, updatedArray, ConfigScope.CurrentUser) + { + AppliesTo = "Az.KeyVault" + }); + Assert.Equal(updatedArray, updated2.Value); + Assert.Equal(updatedArray, cm.GetConfigValueInternal(arrayKey, new InternalInvocationInfo() { ModuleName = "Az.KeyVault" })); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanUpdateConfigForCmdlet() + { + const string warningKey = "DisalbeWarning"; + var warningConfig = new SimpleTypedConfig(warningKey, "", false); + IConfigManager icm = GetConfigManager(warningConfig); + + Assert.False(icm.GetConfigValue(warningKey)); + + ConfigData updated = icm.UpdateConfig(new UpdateConfigOptions(warningKey, true, ConfigScope.CurrentUser) + { + AppliesTo = "Get-AzKeyVault" + }); + Assert.Equal(true, updated.Value); + Assert.False(icm.GetConfigValue(warningKey)); + + var cm = (ConfigManager)icm; + Assert.True(cm.GetConfigValueInternal(warningKey, new InternalInvocationInfo("Az.KeyVault", "Get-AzKeyVault"))); + Assert.False(cm.GetConfigValueInternal(warningKey, new InternalInvocationInfo("Az.Storage", "Get-AzStorageAccount"))); + Assert.False(cm.GetConfigValueInternal(warningKey, new InternalInvocationInfo("Az.KeyVault", "Remove-AzKeyVault"))); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void ThrowWhenOptionIsInvalid() + { + const string key1 = "key"; + var config1 = new SimpleTypedConfig(key1, "", true); + const string key2 = "key2"; + var config2 = new SimpleTypedConfig(key2, "", true); + var icm = GetConfigManager(config1, config2); + + Assert.Throws(() => icm.UpdateConfig(null)); + Assert.Throws(() => icm.UpdateConfig(new UpdateConfigOptions(null, null, ConfigScope.CurrentUser))); + Assert.Throws(() => icm.UpdateConfig(new UpdateConfigOptions(key1, "ThisShouldNotBeAString", ConfigScope.CurrentUser))); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void AppliesToShouldBeCaseInsensitive() + { + const string key = "key"; + var config = new SimpleTypedConfig(key, "", 0); + var icm = GetConfigManager(config); + + icm.UpdateConfig(new UpdateConfigOptions(key, 1, ConfigScope.CurrentUser) { AppliesTo = "az.abc" }); + icm.UpdateConfig(new UpdateConfigOptions(key, 2, ConfigScope.CurrentUser) { AppliesTo = "Az.Abc" }); + Assert.Equal(2, icm.ListConfigs(new ConfigFilter() { Keys = new[] { key }, AppliesTo = "az.abc" }).Single().Value); + Assert.Equal(2, icm.ListConfigs(new ConfigFilter() { Keys = new[] { key }, AppliesTo = "Az.Abc" }).Single().Value); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanUpdateConfigHasSideEffect() + { + int calls = 0; + var mock = new Mock(); + mock.Setup(c => c.Key).Returns("key"); + mock.Setup(c => c.CanApplyTo).Returns(new[] { AppliesTo.Az }); + mock.Setup(c => c.Apply(It.IsAny())).Callback((object v) => + { + switch (++calls) + { + case 1: + Assert.True((bool)v); + break; + case 2: + Assert.False((bool)v); + break; + default: + break; + } + }); + var config = mock.Object; + var icm = GetConfigManager(config); + icm.UpdateConfig(config.Key, true, ConfigScope.CurrentUser); + icm.UpdateConfig(config.Key, false, ConfigScope.CurrentUser); + Assert.Equal(2, calls); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void ShouldNotUpdateConfigIfSideEffectThrows() + { + var config = new ConfigWithSideEffect((bool v) => throw new Exception("oops")); + var icm = GetConfigManager(config); + Assert.Throws(() => icm.UpdateConfig(config.Key, !config.TypedDefaultValue, ConfigScope.CurrentUser)); + Assert.Equal(config.TypedDefaultValue, icm.GetConfigValue(config.Key)); + } + + [Fact] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void ShouldThrowIfAppliesToIsWrong() + { + var key = "OnlyAppliesToAz"; + var config = new SimpleTypedConfig(key, "", true, null, new AppliesTo[] {AppliesTo.Az}); + var icm = GetConfigManager(config); + Assert.Throws(() => icm.UpdateConfig(new UpdateConfigOptions(key, true, ConfigScope.CurrentUser) { AppliesTo = "Az.Accounts" })); + } + + internal class ConfigWithSideEffect : TypedConfig + { + private readonly Action _sideEffect; + + public ConfigWithSideEffect(Action sideEffect) + { + _sideEffect = sideEffect; + } + public override object DefaultValue => true; + + public override string Key => "ConfigWithSideEffect"; + + public override string HelpMessage => "{HelpMessage}"; + + protected override void ApplyTyped(bool value) + { + base.ApplyTyped(value); + _sideEffect(value); + } + } + } +} diff --git a/src/Accounts/Authentication.Test/Mocks/MockDataStore.cs b/src/Accounts/Authentication.Test/Mocks/MockDataStore.cs index 9e0211515c05..6ceaa9fd64d5 100644 --- a/src/Accounts/Authentication.Test/Mocks/MockDataStore.cs +++ b/src/Accounts/Authentication.Test/Mocks/MockDataStore.cs @@ -376,7 +376,8 @@ public Stream OpenForExclusiveWrite(string path) () => { writeLocks[path] = false; - virtualStore[path] = Encoding.Default.GetString(buffer); + // trim \0 otherwise json fails to parse + virtualStore[path] = Encoding.UTF8.GetString(buffer).TrimEnd('\0'); } ); } diff --git a/src/Accounts/Authentication.Test/Mocks/MockEnvironmentVariableProvider.cs b/src/Accounts/Authentication.Test/Mocks/MockEnvironmentVariableProvider.cs new file mode 100644 index 000000000000..85e1ae45c6fa --- /dev/null +++ b/src/Accounts/Authentication.Test/Mocks/MockEnvironmentVariableProvider.cs @@ -0,0 +1,53 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Microsoft.Azure.Commands.Common.Authentication.Config.Internal.Interfaces; + +namespace Microsoft.Azure.PowerShell.Authentication.Test.Mocks +{ + public class MockEnvironmentVariableProvider : IEnvironmentVariableProvider + { + private readonly IDictionary _processVariables = new Dictionary(); + private readonly IDictionary _userVariables = new Dictionary(); + private readonly IDictionary _systemVariables = new Dictionary(); + + public string Get(string variableName, EnvironmentVariableTarget target = EnvironmentVariableTarget.Process) + { + GetVariablesByTarget(target).TryGetValue(variableName, out var result); + return result; + } + + private IDictionary GetVariablesByTarget(EnvironmentVariableTarget target) + { + switch (target) + { + case EnvironmentVariableTarget.Process: + return _processVariables; + case EnvironmentVariableTarget.User: + return _userVariables; + case EnvironmentVariableTarget.Machine: + return _systemVariables; + default: + throw new ArgumentException(nameof(target)); + } + } + + public void Set(string variableName, string value, EnvironmentVariableTarget target = EnvironmentVariableTarget.Process) + { + GetVariablesByTarget(target)[variableName] = value; + } + } +} diff --git a/src/Accounts/Authentication/Properties/AssemblyInfo.cs b/src/Accounts/Authentication/Properties/AssemblyInfo.cs index 5b98cb59484c..5df4f298bba4 100644 --- a/src/Accounts/Authentication/Properties/AssemblyInfo.cs +++ b/src/Accounts/Authentication/Properties/AssemblyInfo.cs @@ -45,3 +45,6 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("2.7.4")] [assembly: AssemblyFileVersion("2.7.4")] +#if !SIGN +[assembly: InternalsVisibleTo("Microsoft.Azure.PowerShell.Authentication.Test")] +#endif From 678bef26a70cf03b192f9201dbec9099c560c597 Mon Sep 17 00:00:00 2001 From: Yeming Liu <11371776+isra-fel@users.noreply.github.com> Date: Thu, 24 Mar 2022 16:56:21 +0800 Subject: [PATCH 5/9] changelog; 3rd party license --- LICENSE.txt | 28 ++++++++++++++++++++++++++++ src/Accounts/Accounts/ChangeLog.md | 6 +++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index b4baf872a8b2..07e000052329 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -273,5 +273,33 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----------------------------------------------------------------------------- +*************** + +The software includes Microsoft.Extensions.Configuration. The MIT License set out below is provided for informational purposes only. It is not the license that governs any part of the software. + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + -------------END OF THIRD PARTY NOTICE---------------------------------------- diff --git a/src/Accounts/Accounts/ChangeLog.md b/src/Accounts/Accounts/ChangeLog.md index e6ab003308e2..fd3ae59ed62c 100644 --- a/src/Accounts/Accounts/ChangeLog.md +++ b/src/Accounts/Accounts/ChangeLog.md @@ -19,6 +19,10 @@ --> ## Upcoming Release +* Added a preview feature allowing user to control the configurations of Azure PowerShell by using the following cmdlets: + - `Get-AzConfig` + - `Update-AzConfig` + - `Clear-AzConfig` * Added `SshCredentialFactory` to support get ssh credential of vm from msal. * Fixed the bug of cmdlet fails when -DefaultProfile is set to service principal login context. [#16617] * Fixed the issue that authorization does not work in Dogfood environment @@ -209,7 +213,7 @@ * Updated Add-AzEnvironment and Set-AzEnvironment to accept parameters AzureAttestationServiceEndpointResourceId and AzureAttestationServiceEndpointSuffix ## Version 1.6.6 -* Add client-side telemetry info for Az 4.0 preview +* Add client-side telemetry info for Az 4.0 `preview` ## Version 1.6.5 * Update references in .psd1 to use relative path From ceebb4a36e4b2f1514d3e0b0d91309d1c34c81f8 Mon Sep 17 00:00:00 2001 From: Yeming Liu <11371776+isra-fel@users.noreply.github.com> Date: Fri, 25 Mar 2022 13:43:35 +0800 Subject: [PATCH 6/9] move PSConfig model to Accounts proj; feedback link --- src/Accounts/Accounts/Accounts.format.ps1xml | 4 ++-- src/Accounts/Accounts/Config/ConfigCommandBase.cs | 2 +- src/Accounts/Accounts/Config/GetConfigCommand.cs | 1 + src/Accounts/Accounts/Config/UpdateConfigCommand.cs | 1 + .../Config => Accounts}/Models/PSConfig.cs | 8 ++++---- 5 files changed, 9 insertions(+), 7 deletions(-) rename src/Accounts/{Authentication/Config => Accounts}/Models/PSConfig.cs (91%) diff --git a/src/Accounts/Accounts/Accounts.format.ps1xml b/src/Accounts/Accounts/Accounts.format.ps1xml index b4f4f5ef762a..e7c23dc3e11c 100644 --- a/src/Accounts/Accounts/Accounts.format.ps1xml +++ b/src/Accounts/Accounts/Accounts.format.ps1xml @@ -277,9 +277,9 @@ - Microsoft.Azure.Commands.Common.Authentication.Config.PSConfig + Microsoft.Azure.Commands.Profile.Models.PSConfig - Microsoft.Azure.Commands.Common.Authentication.Config.PSConfig + Microsoft.Azure.Commands.Profile.Models.PSConfig diff --git a/src/Accounts/Accounts/Config/ConfigCommandBase.cs b/src/Accounts/Accounts/Config/ConfigCommandBase.cs index dc5b43c06b66..3262d0b051f9 100644 --- a/src/Accounts/Accounts/Config/ConfigCommandBase.cs +++ b/src/Accounts/Accounts/Config/ConfigCommandBase.cs @@ -23,7 +23,7 @@ namespace Microsoft.Azure.Commands.Common.Authentication.Config { - [CmdletPreview("The cmdlet group \"AzConfig\" is in preview. Feedback is welcome: https://github.com/Azure/azure-powershell/discussions")] + [CmdletPreview("The cmdlet group \"AzConfig\" is in preview. Feedback is welcome: https://aka.ms/azpsissue")] public abstract class ConfigCommandBase : AzureRMCmdlet { private readonly RuntimeDefinedParameterDictionary _dynamicParameters = new RuntimeDefinedParameterDictionary(); diff --git a/src/Accounts/Accounts/Config/GetConfigCommand.cs b/src/Accounts/Accounts/Config/GetConfigCommand.cs index 9f0147e88e1d..8f675ffae446 100644 --- a/src/Accounts/Accounts/Config/GetConfigCommand.cs +++ b/src/Accounts/Accounts/Config/GetConfigCommand.cs @@ -12,6 +12,7 @@ // limitations under the License. // ---------------------------------------------------------------------------------- +using Microsoft.Azure.Commands.Profile.Models; using Microsoft.Azure.Commands.ResourceManager.Common; using Microsoft.Azure.PowerShell.Common.Config; using System; diff --git a/src/Accounts/Accounts/Config/UpdateConfigCommand.cs b/src/Accounts/Accounts/Config/UpdateConfigCommand.cs index 83fb44f9801d..d8c25df8d4e0 100644 --- a/src/Accounts/Accounts/Config/UpdateConfigCommand.cs +++ b/src/Accounts/Accounts/Config/UpdateConfigCommand.cs @@ -12,6 +12,7 @@ // limitations under the License. // ---------------------------------------------------------------------------------- +using Microsoft.Azure.Commands.Profile.Models; using Microsoft.Azure.PowerShell.Common.Config; using System; using System.Collections.Generic; diff --git a/src/Accounts/Authentication/Config/Models/PSConfig.cs b/src/Accounts/Accounts/Models/PSConfig.cs similarity index 91% rename from src/Accounts/Authentication/Config/Models/PSConfig.cs rename to src/Accounts/Accounts/Models/PSConfig.cs index 782608543971..c211a8fb7d02 100644 --- a/src/Accounts/Authentication/Config/Models/PSConfig.cs +++ b/src/Accounts/Accounts/Models/PSConfig.cs @@ -14,11 +14,11 @@ using Microsoft.Azure.PowerShell.Common.Config; -/// -/// The output model of config-related cmdlets. -/// -namespace Microsoft.Azure.Commands.Common.Authentication.Config +namespace Microsoft.Azure.Commands.Profile.Models { + /// + /// The output model of config-related cmdlets. + /// public class PSConfig { public string Key { get; } From 1a6a670ccd65b078bf46d75077edb5121b7dbba5 Mon Sep 17 00:00:00 2001 From: Yeming Liu <11371776+isra-fel@users.noreply.github.com> Date: Fri, 25 Mar 2022 15:24:17 +0800 Subject: [PATCH 7/9] Move preview attribute into child classes --- src/Accounts/Accounts/Config/ClearConfigCommand.cs | 2 ++ src/Accounts/Accounts/Config/ConfigCommandBase.cs | 4 ++-- src/Accounts/Accounts/Config/GetConfigCommand.cs | 2 ++ src/Accounts/Accounts/Config/UpdateConfigCommand.cs | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Accounts/Accounts/Config/ClearConfigCommand.cs b/src/Accounts/Accounts/Config/ClearConfigCommand.cs index e5ba6206b004..3f4e466202ed 100644 --- a/src/Accounts/Accounts/Config/ClearConfigCommand.cs +++ b/src/Accounts/Accounts/Config/ClearConfigCommand.cs @@ -13,6 +13,7 @@ // ---------------------------------------------------------------------------------- using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.WindowsAzure.Commands.Common.CustomAttributes; using Microsoft.WindowsAzure.Commands.Utilities.Common; using System; using System.Collections.Generic; @@ -24,6 +25,7 @@ namespace Microsoft.Azure.Commands.Common.Authentication.Config { [Cmdlet("Clear", "AzConfig", SupportsShouldProcess = true)] [OutputType(typeof(bool))] + [CmdletPreview(PreviewMessage)] public class ClearConfigCommand : ConfigCommandBase, IDynamicParameters { private const string ClearByKey = "ClearByKey"; diff --git a/src/Accounts/Accounts/Config/ConfigCommandBase.cs b/src/Accounts/Accounts/Config/ConfigCommandBase.cs index 3262d0b051f9..058f53316a7c 100644 --- a/src/Accounts/Accounts/Config/ConfigCommandBase.cs +++ b/src/Accounts/Accounts/Config/ConfigCommandBase.cs @@ -15,7 +15,6 @@ using Microsoft.Azure.Commands.Common.Exceptions; using Microsoft.Azure.Commands.ResourceManager.Common; using Microsoft.Azure.PowerShell.Common.Config; -using Microsoft.WindowsAzure.Commands.Common.CustomAttributes; using System; using System.Collections.Generic; using System.Linq; @@ -23,9 +22,10 @@ namespace Microsoft.Azure.Commands.Common.Authentication.Config { - [CmdletPreview("The cmdlet group \"AzConfig\" is in preview. Feedback is welcome: https://aka.ms/azpsissue")] public abstract class ConfigCommandBase : AzureRMCmdlet { + protected const string PreviewMessage = "The cmdlet group \"AzConfig\" is in preview. Feedback is welcome: https://aka.ms/azpsissue"; + private readonly RuntimeDefinedParameterDictionary _dynamicParameters = new RuntimeDefinedParameterDictionary(); protected IConfigManager ConfigManager { get; } diff --git a/src/Accounts/Accounts/Config/GetConfigCommand.cs b/src/Accounts/Accounts/Config/GetConfigCommand.cs index 8f675ffae446..a6190d87c576 100644 --- a/src/Accounts/Accounts/Config/GetConfigCommand.cs +++ b/src/Accounts/Accounts/Config/GetConfigCommand.cs @@ -15,6 +15,7 @@ using Microsoft.Azure.Commands.Profile.Models; using Microsoft.Azure.Commands.ResourceManager.Common; using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.WindowsAzure.Commands.Common.CustomAttributes; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -25,6 +26,7 @@ namespace Microsoft.Azure.Commands.Common.Authentication.Config { [Cmdlet(VerbsCommon.Get, AzureRMConstants.AzureRMPrefix + "Config")] [OutputType(typeof(PSConfig))] + [CmdletPreview(PreviewMessage)] public class GetConfigCommand : ConfigCommandBase, IDynamicParameters { public GetConfigCommand() : base() diff --git a/src/Accounts/Accounts/Config/UpdateConfigCommand.cs b/src/Accounts/Accounts/Config/UpdateConfigCommand.cs index d8c25df8d4e0..c1946b6099e7 100644 --- a/src/Accounts/Accounts/Config/UpdateConfigCommand.cs +++ b/src/Accounts/Accounts/Config/UpdateConfigCommand.cs @@ -14,6 +14,7 @@ using Microsoft.Azure.Commands.Profile.Models; using Microsoft.Azure.PowerShell.Common.Config; +using Microsoft.WindowsAzure.Commands.Common.CustomAttributes; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -24,6 +25,7 @@ namespace Microsoft.Azure.Commands.Common.Authentication.Config { [Cmdlet("Update", "AzConfig", SupportsShouldProcess = true)] [OutputType(typeof(PSConfig))] + [CmdletPreview(PreviewMessage)] public class UpdateConfigCommand : ConfigCommandBase, IDynamicParameters { private const string ProcessMessage = "Update the configs that apply to \"{0}\" by the following keys: {1}."; From 4f6a903f8a2bcab378b93d1bf4331b3cc3691cd5 Mon Sep 17 00:00:00 2001 From: Yeming Liu <11371776+isra-fel@users.noreply.github.com> Date: Wed, 30 Mar 2022 17:05:44 +0800 Subject: [PATCH 8/9] fix get/update by config key --- src/Accounts/Accounts/Config/ClearConfigCommand.cs | 4 +++- src/Accounts/Accounts/Config/GetConfigCommand.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Accounts/Accounts/Config/ClearConfigCommand.cs b/src/Accounts/Accounts/Config/ClearConfigCommand.cs index 3f4e466202ed..f246db7495b2 100644 --- a/src/Accounts/Accounts/Config/ClearConfigCommand.cs +++ b/src/Accounts/Accounts/Config/ClearConfigCommand.cs @@ -78,7 +78,9 @@ public override void ExecuteCmdlet() private void ClearConfigByKey() { - IEnumerable configKeysFromInput = GetConfigsSpecifiedByUser().Where(x => (bool)x.Value).Select(x => x.Key); + IEnumerable configKeysFromInput = GetConfigsSpecifiedByUser() + .Where(x => (SwitchParameter)x.Value) + .Select(x => x.Key); if (!configKeysFromInput.Any()) { WriteWarning($"Please specify the key(s) of the configs to clear. Run `help {MyInvocation.MyCommand.Name}` for more information."); diff --git a/src/Accounts/Accounts/Config/GetConfigCommand.cs b/src/Accounts/Accounts/Config/GetConfigCommand.cs index a6190d87c576..b285b9b1f544 100644 --- a/src/Accounts/Accounts/Config/GetConfigCommand.cs +++ b/src/Accounts/Accounts/Config/GetConfigCommand.cs @@ -57,7 +57,9 @@ public override void ExecuteCmdlet() private ConfigFilter CreateConfigFilter() { ConfigFilter filter = new ConfigFilter() { AppliesTo = AppliesTo }; - IEnumerable configKeysFromInput = GetConfigsSpecifiedByUser().Where(x => (bool)x.Value).Select(x => x.Key); + IEnumerable configKeysFromInput = GetConfigsSpecifiedByUser() + .Where(x => (SwitchParameter)x.Value) + .Select(x => x.Key); if (configKeysFromInput.Any()) { filter.Keys = configKeysFromInput; From 1283ed6ed4f8a44ffea0a2bd51a93a61ac18a9b8 Mon Sep 17 00:00:00 2001 From: Yeming Liu <11371776+isra-fel@users.noreply.github.com> Date: Mon, 9 May 2022 11:13:01 +0800 Subject: [PATCH 9/9] correct cmdlet regex --- .../PSNamingUtilitiesTests.cs | 69 +++++++++++++++++++ .../Authentication/Authentication.csproj | 4 -- .../Config/ConfigInitializer.cs | 10 --- .../Config/Helper/AppliesToHelper.cs | 10 +-- .../Utilities/PSNamingUtilities.cs | 64 +++++++++++++++++ 5 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 src/Accounts/Authentication.Test/PSNamingUtilitiesTests.cs create mode 100644 src/Accounts/Authentication/Utilities/PSNamingUtilities.cs diff --git a/src/Accounts/Authentication.Test/PSNamingUtilitiesTests.cs b/src/Accounts/Authentication.Test/PSNamingUtilitiesTests.cs new file mode 100644 index 000000000000..3d63daf59c93 --- /dev/null +++ b/src/Accounts/Authentication.Test/PSNamingUtilitiesTests.cs @@ -0,0 +1,69 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using Microsoft.Azure.Commands.Common.Authentication.Utilities; +using Microsoft.Rest.ClientRuntime.Azure.TestFramework; +using Xunit; + +namespace Microsoft.Azure.Commands.Common.Authentication.Test +{ + public class PSNamingUtilitiesTests + { + [Theory] + [InlineData("Az.Accounts", true)] + [InlineData("aZ.cOMPUTE", true)] + [InlineData("az.stackhci", true)] + [InlineData("", false)] + [InlineData("AzureRM.Profile", false)] + [InlineData("Az", false)] + [InlineData("AzAccounts", false)] + [InlineData("Get-AzContext", false)] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanRecognizeModuleName(string name, bool expected) + { + Assert.Equal(expected, PSNamingUtilities.IsModuleName(name)); + } + + [Theory] + [InlineData("Get-AzContext", true)] + [InlineData("update-azstorageaccount", true)] + [InlineData("Remove-AzEverything", true)] + [InlineData("Get-AzDataFactoryV2", true)] + [InlineData("", false)] + [InlineData("Az.Accounts", false)] + [InlineData("Az", false)] + [InlineData("NewAzVM", false)] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanRecognizeCmdletName(string name, bool expected) + { + Assert.Equal(expected, PSNamingUtilities.IsCmdletName(name)); + } + + [Theory] + [InlineData("Az.Accounts", true)] + [InlineData("aZ.cOMPUTE", true)] + [InlineData("az.stackhci", true)] + [InlineData("Get-AzContext", true)] + [InlineData("Remove-AzEverything", true)] + [InlineData("Get-AzDataFactoryV2", true)] + [InlineData("", false)] + [InlineData("Az", false)] + [InlineData("NewAzVM", false)] + [Trait(TestTraits.AcceptanceType, TestTraits.CheckIn)] + public void CanRecognizeModuleOrCmdletName(string name, bool expected) + { + Assert.Equal(expected, PSNamingUtilities.IsModuleOrCmdletName(name)); + } + } +} diff --git a/src/Accounts/Authentication/Authentication.csproj b/src/Accounts/Authentication/Authentication.csproj index e37d65960075..388c5e5f8e2e 100644 --- a/src/Accounts/Authentication/Authentication.csproj +++ b/src/Accounts/Authentication/Authentication.csproj @@ -30,8 +30,4 @@ - - - - diff --git a/src/Accounts/Authentication/Config/ConfigInitializer.cs b/src/Accounts/Authentication/Config/ConfigInitializer.cs index d179bbe9fc89..cf975f8fe906 100644 --- a/src/Accounts/Authentication/Config/ConfigInitializer.cs +++ b/src/Accounts/Authentication/Config/ConfigInitializer.cs @@ -141,16 +141,6 @@ internal void InitializeForAzureSession(AzureSession session) private void RegisterConfigs(IConfigManager configManager) { - // simple configs - // todo: review the help messages - //configManager.RegisterConfig(new SimpleTypedConfig(ConfigKeys.SuppressWarningMessage, "Controls if the warning messages of upcoming breaking changes are enabled or suppressed. The messages are typically displayed when a cmdlet that will have breaking change in the future is executed.", false, BreakingChangeAttributeHelper.SUPPRESS_ERROR_OR_WARNING_MESSAGE_ENV_VARIABLE_NAME)); - //configManager.RegisterConfig(new SimpleTypedConfig(ConfigKeys.EnableInterceptSurvey, "When enabled, a message of taking part in the survey about the user experience of Azure PowerShell will prompt at low frequency.", true, "Azure_PS_Intercept_Survey")); - // todo: when the input is not a valid subscription name or id. Connect-AzAccount will throw an error. Is it right? - //configManager.RegisterConfig(new SimpleTypedConfig(ConfigKeys.DefaultSubscriptionForLogin, "Subscription name or GUID. If defined, when logging in Azure PowerShell without specifying the subscription, this one will be used to select the default context.", string.Empty)); - // todo: add later - //configManager.RegisterConfig(new RetryConfig()); - // todo: how to migrate old config - //configManager.RegisterConfig(new EnableDataCollectionConfig()); } } } diff --git a/src/Accounts/Authentication/Config/Helper/AppliesToHelper.cs b/src/Accounts/Authentication/Config/Helper/AppliesToHelper.cs index 32902f509faa..b336d19076ff 100644 --- a/src/Accounts/Authentication/Config/Helper/AppliesToHelper.cs +++ b/src/Accounts/Authentication/Config/Helper/AppliesToHelper.cs @@ -12,12 +12,12 @@ // limitations under the License. // ---------------------------------------------------------------------------------- +using Microsoft.Azure.Commands.Common.Authentication.Utilities; using Microsoft.Azure.PowerShell.Common.Config; using System; using System.Collections.Generic; using System.Linq; using System.Text; -using System.Text.RegularExpressions; namespace Microsoft.Azure.Commands.Common.Authentication.Config { @@ -26,10 +26,6 @@ namespace Microsoft.Azure.Commands.Common.Authentication.Config /// public static class AppliesToHelper { - internal static readonly Regex ModulePattern = new Regex(@"^az\.[a-z]+$", RegexOptions.IgnoreCase); - internal static readonly Regex CmdletPattern = new Regex(@"^[a-z]+-[a-z]+$", RegexOptions.IgnoreCase); - internal static readonly Regex ModuleOrCmdletPattern = new Regex(@"^az\.[a-z]+$|^[a-z]+-[a-z]+$", RegexOptions.IgnoreCase); - /// /// Tries to parse a user-input text to an enum. /// @@ -44,13 +40,13 @@ public static bool TryParseAppliesTo(string text, out AppliesTo appliesTo) return true; } - if (ModulePattern.IsMatch(text)) + if (PSNamingUtilities.IsModuleName(text)) { appliesTo = AppliesTo.Module; return true; } - if (CmdletPattern.IsMatch(text)) + if (PSNamingUtilities.IsCmdletName(text)) { appliesTo = AppliesTo.Cmdlet; return true; diff --git a/src/Accounts/Authentication/Utilities/PSNamingUtilities.cs b/src/Accounts/Authentication/Utilities/PSNamingUtilities.cs new file mode 100644 index 000000000000..e4136f753937 --- /dev/null +++ b/src/Accounts/Authentication/Utilities/PSNamingUtilities.cs @@ -0,0 +1,64 @@ +// ---------------------------------------------------------------------------------- +// +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +using System.Text.RegularExpressions; + +namespace Microsoft.Azure.Commands.Common.Authentication.Utilities +{ + /// + /// Utility class about PowerShell naming (cmdlet name, module name). + /// + /// + /// All the mothods are within Azure PowerShell context, for example, module name should start with "Az.". + /// + public static class PSNamingUtilities + { + private static readonly Regex ModulePattern = new Regex(@"^az\.[a-z]+$", RegexOptions.IgnoreCase); + private static readonly Regex CmdletPattern = new Regex(@"^[a-z]+-[a-z\d]+$", RegexOptions.IgnoreCase); + private static readonly Regex ModuleOrCmdletPattern = new Regex(@"^az\.[a-z]+$|^[a-z]+-[a-z\d]+$", RegexOptions.IgnoreCase); + + /// + /// Returns if the given is a valid module name. + /// + /// + /// This method only does pattern-matching. It does not check if the name is real. + /// + public static bool IsModuleName(string moduleName) + { + return ModulePattern.IsMatch(moduleName); + } + + /// + /// Returns if the given is a valid cmdlet name. + /// + /// + /// This method only does pattern-matching. It does not check if the name is real. + /// + public static bool IsCmdletName(string cmdletName) + { + return CmdletPattern.IsMatch(cmdletName); + } + + /// + /// Returns if the given is a valid module name or cmdlet name. + /// + /// + /// This method only does pattern-matching. It does not check if the name is real. + /// + public static bool IsModuleOrCmdletName(string moduleOrCmdletName) + { + return ModuleOrCmdletPattern.IsMatch(moduleOrCmdletName); + } + } +}