Skip to content

Multi Profile Support - Part 1 #254

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 29, 2020
4 changes: 3 additions & 1 deletion src/Authentication/Authentication/Cmdlets/ConnectGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ protected override void ProcessRecord()
else
clientApplication = (authProvider as ClientCredentialProvider).ClientApplication;

// Incremental scope consent without re-instanciating the auth provider. We will use a static instance.
// Incremental scope consent without re-instantiating the auth provider. We will use a static instance.
GraphRequestContext graphRequestContext = new GraphRequestContext();
graphRequestContext.CancellationToken = cancellationToken;
graphRequestContext.MiddlewareOptions = new Dictionary<string, IMiddlewareOption>
Expand Down Expand Up @@ -170,6 +170,7 @@ private void ThrowParameterError(string parameterName)
/// </summary>
public void OnImport()
{
// TODO: Consider checking for a persisted copy of GraphSession or settings on disk.
GraphSessionInitializer.InitializeSession();
}

Expand All @@ -179,6 +180,7 @@ public void OnImport()
/// <param name="psModuleInfo">A <see cref="PSModuleInfo"/> object.</param>
public void OnRemove(PSModuleInfo psModuleInfo)
{
// TODO: Consider persisting the GraphSession or settings on disk.
GraphSession.Reset();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets

[Cmdlet(VerbsCommon.Get, "MgContext", DefaultParameterSetName = Constants.UserParameterSet)]
[OutputType(typeof(IAuthContext))]
public class GetMGContext: PSCmdlet
public class GetMgContext: PSCmdlet
{
protected override void BeginProcessing()
{
Expand Down
48 changes: 48 additions & 0 deletions src/Authentication/Authentication/Cmdlets/GetMgProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets
{
using Microsoft.Graph.PowerShell.Authentication.Extensions;
using Microsoft.Graph.PowerShell.Authentication.Models;
using System;
using System.Linq;
using System.Management.Automation;
using static Microsoft.Graph.PowerShell.Authentication.Common.GraphProfile;

[Cmdlet(VerbsCommon.Get, "MgProfile")]
[OutputType(typeof(PSGraphServiceProfile))]
public class GetMgProfile: PSCmdlet
{
[Parameter(Mandatory = false)]
[ValidateNotNullOrEmpty]
public string[] ModuleName { get; set; }

[Parameter(Mandatory = false)]
public SwitchParameter ListAvailable { get; set; }

protected override void ProcessRecord()
{
base.ProcessRecord();
try
{
bool isModuleNameBound = this.IsBound(nameof(ModuleName));
bool isListAvailableBound = this.IsBound(nameof(ListAvailable));
string[] moduleNames = isModuleNameBound ? ModuleName : new string[] { };
string[] profiles = isModuleNameBound || isListAvailableBound
? GetProfiles(InvokeCommand, isListAvailableBound, moduleNames)
: new string[] { GraphSession.Instance.SelectedProfile };
if (profiles.Any((p) => !string.IsNullOrWhiteSpace(p)))
{
WriteObject(profiles.Where((profile) => !string.IsNullOrWhiteSpace(profile))
.Select((p) => PSGraphServiceProfile.Create(p)), true);
}
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, string.Empty, ErrorCategory.CloseError, null));
}
}
}
}
54 changes: 54 additions & 0 deletions src/Authentication/Authentication/Cmdlets/SelectMgProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets
{
using Microsoft.Graph.PowerShell.Authentication.Extensions;
using System;
using System.Linq;
using System.Management.Automation;
using static Microsoft.Graph.PowerShell.Authentication.Common.GraphProfile;

/// <summary>
/// Select the current Microsoft Graph profile.
/// </summary>
[Cmdlet(VerbsCommon.Select, "MgProfile", SupportsShouldProcess = true)]
[OutputType(typeof(bool))]
public class SelectMgProfile: PSCmdlet
{
[Parameter(Mandatory = true)]
[Alias("ProfileName")]
[ValidateNotNullOrEmpty]
public string Name { get; set; }

[Parameter]
public SwitchParameter PassThru { get; set; }

protected override void ProcessRecord()
{
base.ProcessRecord();
try
{
if (this.IsParameterBound(c => c.Name))
{
PSModuleInfo[] modules = GetModules(InvokeCommand).Where(m => GetProfiles(m).Contains(Name)).ToArray();
string moduleList = string.Join(", ", modules.Select(m => m.Name));
if (ShouldProcess($"Modules {moduleList}", $"Load modules with profile {Name}"))
{
GraphSession.Instance.SelectedProfile = Name;
ReloadModules(InvokeCommand, modules);
if (PassThru.IsPresent && PassThru.ToBool())
{
WriteObject(true);
}
}
}
}
catch (Exception ex)
{
WriteError(new ErrorRecord(ex, string.Empty, ErrorCategory.CloseError, null));
}
}
}
}
61 changes: 61 additions & 0 deletions src/Authentication/Authentication/Common/GraphProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

namespace Microsoft.Graph.PowerShell.Authentication.Common
{
using System;
using System.Management.Automation;
using System.Linq;
using System.Collections;
using System.Collections.ObjectModel;
using System.IO;

/// <summary>
/// Methods for working with Microsoft Graph profiles.
/// </summary>
internal static class GraphProfile
{
public static string[] GetProfiles(CommandInvocationIntrinsics invokeCommand, bool listAvailable = false, params string[] moduleNames)
{
return GetModules(invokeCommand, listAvailable, moduleNames).SelectMany(GetProfiles).Distinct().ToArray();
}

public static PSModuleInfo[] GetModules(CommandInvocationIntrinsics invokeCommand, bool listAvailable = false, params string[] moduleNames)
{
string nameParameter = $" -Name { (moduleNames != null && moduleNames.Any() ? GetCommaSeparatedQuotedList(moduleNames) : "Microsoft.Graph*" )}";
string listAvailableParameter = listAvailable ? " -ListAvailable" : String.Empty;
string command = $"Get-Module{nameParameter}{listAvailableParameter}";
Collection<PSObject> modules = listAvailable ? PowerShell.Create().AddScript(command).Invoke<PSObject>() : invokeCommand.NewScriptBlock(command).Invoke();
return modules != null ? modules.Select(m => m?.BaseObject as PSModuleInfo).Where(m => m != null).ToArray() : new PSModuleInfo[] { };
}

public static string[] GetProfiles(PSModuleInfo moduleInfo)
{
var moduleProfileInfo = (moduleInfo?.PrivateData as Hashtable)?["Profiles"];
var moduleProfiles = moduleProfileInfo as object[] ?? (moduleProfileInfo != null ? new[] { moduleProfileInfo } : null);
return moduleProfiles != null && moduleProfiles.Any() ? moduleProfiles.Cast<string>().ToArray() : new string[] { };
}

public static void ReloadModules(CommandInvocationIntrinsics invokeCommand, params PSModuleInfo[] moduleInfos)
{
var modulePaths = GetCommaSeparatedQuotedList(moduleInfos.Select(GetModulePath).ToArray());
if (!String.IsNullOrEmpty(modulePaths))
{
var command = $"Import-Module -Name {modulePaths} -Force";
invokeCommand.NewScriptBlock(command).Invoke();
}
}

private static string GetCommaSeparatedQuotedList(params string[] items)
{
return string.Join(", ", items.Where(i => !string.IsNullOrEmpty(i)).Select(i => $"'{i}'"));
}

private static string GetModulePath(PSModuleInfo moduleInfo)
{
var scriptPsd1 = Path.Combine(moduleInfo.ModuleBase, $"{moduleInfo.Name}.psd1");
return moduleInfo.ModuleType == ModuleType.Script && File.Exists(scriptPsd1) ? scriptPsd1 : moduleInfo.Path;
}
}
}
9 changes: 9 additions & 0 deletions src/Authentication/Authentication/Common/GraphSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@ public class GraphSession : IGraphSession
static ReaderWriterLockSlim sessionLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
internal Guid _graphSessionId;

// Checks if an instance of <see cref="GraphSession"/> exists.
public static bool Exists { get { return _initialized; } }

/// <summary>
/// Gets or Sets <see cref="IAuthContext"/>.
/// </summary>
public IAuthContext AuthContext { get; set; }

/// <summary>
/// The name of the selected Microsoft Graph profile.
/// This defaults to v1.0-beta.
/// </summary>
public string SelectedProfile { get; set; } = "v1.0-beta";

/// <summary>
/// Gets an instance of <see cref="GraphSession"/>.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/Authentication/Authentication/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static class Constants
internal const string AppParameterSet = "AppParameterSet";
internal const int MaxDeviceCodeTimeOut = 120; // 2 mins timeout.
internal static readonly string TokenCacheDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".graph");
internal const string ProfileDescription = "A snapshot of the Microsoft Graph {0} API for {1} cloud.";
internal const string TokenCacheServiceName = "com.microsoft.graph.powershell.sdkcache";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

namespace Microsoft.Graph.PowerShell.Authentication.Extensions
{
using System;
using System.Collections.Generic;
internal static class IEnumerableExtensions
{
/// <summary>
/// Perform an action on each element of a sequence.
/// </summary>
/// <typeparam name="T">Type of elements in the sequence.</typeparam>
/// <param name="sequence">The sequence.</param>
/// <param name="action">The action to perform.</param>
public static void ForEach<T>(this IEnumerable<T> sequence, Action<T> action)
{
foreach (T element in sequence)
{
action(element);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

namespace Microsoft.Graph.PowerShell.Authentication.Extensions
{
using Microsoft.Graph.PowerShell.Authentication.Helpers;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq.Expressions;
using System.Management.Automation;
internal static class PSCmdletExtensions
{
/// <summary>
/// Executes a PowerShell script.
/// </summary>
/// <typeparam name="T">The output type to return.</typeparam>
/// <param name="cmdlet">The executing cmdlet.</param>
/// <param name="contents">The PowerShell scrip to execute.</param>
/// <returns>The result for the executed script.</returns>
public static List<T> ExecuteScript<T>(this PSCmdlet cmdlet, string contents)
{
List<T> output = new List<T>();

using (PowerShell powershell = PowerShell.Create(RunspaceMode.CurrentRunspace))
{
powershell.AddScript(contents);
Collection<T> result = powershell.Invoke<T>();

if (cmdlet.SessionState != null)
{
powershell.Streams.Error.ForEach(e => cmdlet.WriteError(e));
powershell.Streams.Verbose.ForEach(r => cmdlet.WriteVerbose(r.Message));
powershell.Streams.Warning.ForEach(r => cmdlet.WriteWarning(r.Message));
}

if (result != null && result.Count > 0)
{
output.AddRange(result);
}
}

return output;
}

/// <summary>
/// Determines is a parameter has been provided by the user.
/// </summary>
/// <param name="cmdlet">The executing cmdlet.</param>
/// <param name="parameterName">The name of the parameter to check.</param>
/// <returns>True is the parameter was set by the user, otherwise false.</returns>
public static bool IsBound(this PSCmdlet cmdlet, string parameterName)
{
return cmdlet.MyInvocation?.BoundParameters.ContainsKey(parameterName) ?? false;
}

/// <summary>
/// Determines is a parameter has been provided by the user.
/// </summary>
/// <typeparam name="TPSCmdlet">Cmdlet type.</typeparam>
/// <typeparam name="TProp">Property type.</typeparam>
/// <param name="cmdlet">The executing cmdlet.</param>
/// <param name="propertySelector">The parameter to check</param>
/// <returns>True is the parameter was set by the user, otherwise false.</returns>
public static bool IsParameterBound<TPSCmdlet, TProp>(this TPSCmdlet cmdlet, Expression<Func<TPSCmdlet, TProp>> propertySelector) where TPSCmdlet : PSCmdlet
{
var propName = ((MemberExpression)propertySelector.Body).Member.Name;
return cmdlet.IsBound(propName);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

namespace Microsoft.Graph.PowerShell.Authentication.Extensions
{
using System;
public static class StringExtensions
{
/// <summary>
/// Indicates whether a specified string is null, empty, consists only of white-space, or has the specified search value.
/// </summary>
/// <param name="target">The target string to look in.</param>
/// <param name="searchValue">The substring to seek.</param>
/// <param name="comparison">The <see cref="StringComparison"/> to use. This defaults to <see cref="StringComparison.OrdinalIgnoreCase"/>.</param>
/// <returns>true if the searchValue parameter occurs within this string; otherwise false.</returns>
public static bool ContainsNotNull(this string target, string searchValue, StringComparison comparison = StringComparison.OrdinalIgnoreCase)
{
if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(searchValue))
{
return false;
}

switch (comparison)
{
case StringComparison.CurrentCultureIgnoreCase:
case StringComparison.OrdinalIgnoreCase:
target = target.ToLower();
searchValue = searchValue.ToLower();
break;
case StringComparison.InvariantCultureIgnoreCase:
target = target.ToLowerInvariant();
searchValue = searchValue.ToLowerInvariant();
break;
}

return target.Contains(searchValue);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ FormatsToProcess = './Microsoft.Graph.Authentication.format.ps1xml'
FunctionsToExport = @()

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = 'Connect-Graph', 'Disconnect-Graph', 'Get-MgContext'
CmdletsToExport = 'Connect-Graph', 'Disconnect-Graph', 'Get-MgContext', 'Get-MgProfile', 'Select-MgProfile'

# Variables to export from this module
# VariablesToExport = @()
Expand Down
Loading