Skip to content

Commit

Permalink
Addresses #3
Browse files Browse the repository at this point in the history
  • Loading branch information
jmprieur committed Dec 7, 2020
1 parent ca1bf5b commit 65c86a0
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 21 deletions.
1 change: 1 addition & 0 deletions src/DotnetTool/DeveloperCredentials/MsalTokenCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext r
}
catch (MsalUiRequiredException ex)
{
Console.WriteLine("Please re-sign-in in Visual Studio");
result = await app.AcquireTokenInteractive(requestContext.Scopes)
.WithLoginHint(Username)
.WithClaims(ex.Claims)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,38 @@ await AddPasswordCredentials(
return effectiveApplicationParameters;
}

internal async Task UpdateApplication(TokenCredential tokenCredential, ApplicationParameters reconcialedApplicationParameters)
{
var graphServiceClient = GetGraphServiceClient(tokenCredential);

var existingApplication = (await graphServiceClient.Applications
.Request()
.Filter($"appId eq '{reconcialedApplicationParameters.ClientId}'")
.GetAsync()).First();

// Updates the redirect URIs
var updatedApp = new Application
{
Web = existingApplication.Web
};
updatedApp.Web.RedirectUris = reconcialedApplicationParameters.WebRedirectUris;

// TODO: update other fields.
// See https://github.com/jmprieur/app-provisonning-tool/issues/10
await graphServiceClient.Applications[existingApplication.Id]
.Request()
.UpdateAsync(updatedApp);

if (existingApplication.RequiredResourceAccess == null
|| existingApplication.RequiredResourceAccess.Any()
|| existingApplication.PasswordCredentials == null
|| !existingApplication.PasswordCredentials.Any()
|| !existingApplication.PasswordCredentials.Any(password => string.IsNullOrEmpty(password.SecretText)))
{
await AddPasswordCredentials(graphServiceClient, existingApplication, reconcialedApplicationParameters);
}
}

private async Task AddApiPermissionFromBlazorwasmHostedSpaToServerApi(
GraphServiceClient graphServiceClient,
Application createdApplication,
Expand Down Expand Up @@ -253,6 +285,8 @@ private static async Task AddAdminConsentToApiPermissions(
Scope = string.Join(" ", resourceAndScopes.Select(r => r.Scope))
};

// TODO: See https://github.com/jmprieur/app-provisonning-tool/issues/9.
// We need to process the case where the developer is not a tenant admin
await graphServiceClient.Oauth2PermissionGrants
.Request()
.AddAsync(oAuth2PermissionGrant);
Expand Down Expand Up @@ -525,11 +559,11 @@ private ApplicationParameters GetEffectiveApplicationParameters(
IsWebApp = application.Web != null,
TenantId = tenant.Id,
Domain = tenant.VerifiedDomains.FirstOrDefault(v => v.IsDefault.HasValue && v.IsDefault.Value)?.Name,
CallsMicrosoftGraph = application.RequiredResourceAccess.Any(r => r.ResourceAppId == MicrosoftGraphAppId),
CallsMicrosoftGraph = application.RequiredResourceAccess.Any(r => r.ResourceAppId == MicrosoftGraphAppId) && !isB2C,
CallsDownstreamApi = application.RequiredResourceAccess.Any(r => r.ResourceAppId != MicrosoftGraphAppId),
LogoutUrl = application.Web?.LogoutUrl,

// Parameters that cannot be infered from the app
// Parameters that cannot be infered from the registered app
SusiPolicy = originalApplicationParameters.SusiPolicy,
SecretsId = originalApplicationParameters.SecretsId,
TargetFramework = originalApplicationParameters.TargetFramework,
Expand Down
9 changes: 9 additions & 0 deletions src/DotnetTool/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
"webapp-singleorg-callswebapi": {
"commandName": "Project",
"workingDirectory": "C:\\Users\\jmprieur\\source\\repos\\github\\jmprieur\\app-provisonning-tool\\src\\UnitTests\\bin\\Debug\\net5.0\\Tests\\webapp\\webapp-singleorg-callswebapi"
},
"webapp-from-existing-app": {
"commandName": "Project",
"workingDirectory": "C:\\temp\\test-provisionning\\Tests\\webapp\\webapp-singleorg-callsgraph",
"commandLineArgs": "--client-id a727877b-0e1b-475c-afac-69bbca2779f6 --tenant-id testprovisionningtool.onmicrosoft.com"
},
"update-webapp-from-existing-app": {
"commandName": "Project",
"workingDirectory": "C:\\temp\\test-provisionning\\Tests\\webapp\\webapp-singleorg-callsgraph"
}
}
}
53 changes: 36 additions & 17 deletions src/DotnetTool/Tool/AppProvisionningTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace DotnetTool
Expand Down Expand Up @@ -68,29 +69,27 @@ public async Task Run()
tokenCredential,
projectSettings.ApplicationParameters);




// Reconciliate code configuration and app registration
ApplicationParameters reconcialedApplicationParameters = Reconciliate(
bool appNeedsUpdate = Reconciliate(
projectSettings.ApplicationParameters,
effectiveApplicationParameters);

// Write code configuration and/or app registration
// Update appp registration if needed
Summary summary = new Summary();
WriteProjectConfiguration(
summary,
projectSettings,
reconcialedApplicationParameters);

if (reconcialedApplicationParameters != effectiveApplicationParameters)
if (appNeedsUpdate)
{
WriteApplicationRegistration(
await WriteApplicationRegistration(
summary,
reconcialedApplicationParameters,
effectiveApplicationParameters,
tokenCredential);
}

// Write code configuration if needed
WriteProjectConfiguration(
summary,
projectSettings,
effectiveApplicationParameters);

// Summarizes what happened
WriteSummary(summary);
}
Expand All @@ -104,9 +103,10 @@ private void WriteSummary(Summary summary)
}
}

private void WriteApplicationRegistration(Summary summary, ApplicationParameters reconcialedApplicationParameters, TokenCredential tokenCredential)
private async Task WriteApplicationRegistration(Summary summary, ApplicationParameters reconcialedApplicationParameters, TokenCredential tokenCredential)
{
summary.changes.Add(new Change($"Writing the project AppId = {reconcialedApplicationParameters.ClientId}"));
await MicrosoftIdentityPlatformApplicationManager.UpdateApplication(tokenCredential, reconcialedApplicationParameters);
}

private void WriteProjectConfiguration(Summary summary, ProjectAuthenticationSettings projectSettings, ApplicationParameters reconcialedApplicationParameters)
Expand All @@ -115,10 +115,27 @@ private void WriteProjectConfiguration(Summary summary, ProjectAuthenticationSet
codeWriter.WriteConfiguration(summary, projectSettings.Replacements, reconcialedApplicationParameters);
}

private ApplicationParameters Reconciliate(ApplicationParameters applicationParameters, ApplicationParameters effectiveApplicationParameters)
private bool Reconciliate(ApplicationParameters applicationParameters, ApplicationParameters effectiveApplicationParameters)
{
Console.WriteLine(nameof(Reconciliate));
return effectiveApplicationParameters;
// Redirect Uris that are needed by the code, but not yet registered
IEnumerable<string> missingRedirectUri = applicationParameters.WebRedirectUris.Except(effectiveApplicationParameters.WebRedirectUris);

bool needUpdate = missingRedirectUri.Any();

if (needUpdate)
{
effectiveApplicationParameters.WebRedirectUris.AddRange(missingRedirectUri);
}

// TODO:
// See also https://github.com/jmprieur/app-provisonning-tool/issues/10
/*
string? audience = ComputeAudienceToSet(applicationParameters.SignInAudience, effectiveApplicationParameters.SignInAudience);
IEnumerable<ApiPermission> missingApiPermission = null;
IEnumerable<string> missingExposedScopes = null;
bool needUpdate = missingRedirectUri != null || audience != null || missingApiPermission != null || missingExposedScopes != null;
*/
return needUpdate;
}

private async Task<ApplicationParameters> ReadOrProvisionMicrosoftIdentityApplication(TokenCredential tokenCredential, ApplicationParameters applicationParameters)
Expand Down Expand Up @@ -150,6 +167,8 @@ private ProjectAuthenticationSettings InferApplicationParameters(
CodeReader reader = new CodeReader();
ProjectAuthenticationSettings projectSettings = reader.ReadFromFiles(provisioningToolOptions.CodeFolder, projectDescription, projectDescriptions);
projectSettings.ApplicationParameters.DisplayName ??= Path.GetFileName(provisioningToolOptions.CodeFolder);
projectSettings.ApplicationParameters.ClientId ??= provisioningToolOptions.ClientId;
projectSettings.ApplicationParameters.TenantId ??= provisioningToolOptions.TenantId;
return projectSettings;
}

Expand Down
2 changes: 1 addition & 1 deletion src/DotnetTool/msIdentityApp.csproj.user
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ShowAllFiles>false</ShowAllFiles>
<ActiveDebugProfile>webapp-singleorg-callswebapi</ActiveDebugProfile>
<ActiveDebugProfile>webapp-from-existing-app</ActiveDebugProfile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
Expand Down
91 changes: 90 additions & 1 deletion src/UnitTests/E2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public E2ETests(ITestOutputHelper output)
[InlineData("blazorwasm\\blazorwasm-b2c", "dotnet new blazorwasm --auth IndividualB2C")]
[InlineData("blazorwasm2\\blazorwasm2-b2c-hosted", "dotnet new blazorwasm --auth IndividualB2C --hosted")]
[Theory]
public async Task TestEndToEnd(string folder, string command)
public async Task TestNewAppEndToEnd(string folder, string command)
{
// Create the folder
string executionFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
Expand Down Expand Up @@ -132,5 +132,94 @@ private void RunProcess(string command, string folderToCreate, string postFix=""
testOutput.WriteLine(errors);
Assert.Equal(string.Empty, errors);
}

//[InlineData("webapp\\webapp-noauth", "dotnet new webapp")]
//[InlineData("webapp\\webapp-singleorg", "dotnet new webapp --auth SingleOrg")]
[InlineData("webapp\\webapp-singleorg-callsgraph", "dotnet new webapp --auth SingleOrg --calls-graph")]
//[InlineData("webapp\\webapp-singleorg-callswebapi", "dotnet new webapp --auth SingleOrg --called-api-url \"https://graph.microsoft.com/beta/me\" --called-api-scopes \"user.read\"")]
//[InlineData("webapp\\webapp-b2c", "dotnet new webapp --auth IndividualB2C")]
//[InlineData("webapp\\webapp-b2c-callswebapi", "dotnet new webapp --auth IndividualB2C --called-api-url \"https://fabrikamb2chello.azurewebsites.net/hello\" --called-api-scopes \"https://fabrikamb2c.onmicrosoft.com/helloapi/demo.read\"")]
//[InlineData("webapi\\webapi-noauth", "dotnet new webapi")]
//[InlineData("webapi\\webapi-singleorg", "dotnet new webapi --auth SingleOrg")]
//[InlineData("webapi\\webapi-singleorg-callsgraph", "dotnet new webapi --auth SingleOrg --calls-graph")]
//[InlineData("webapi\\webapi-singleorg-callswebapi", "dotnet new webapi --auth SingleOrg --called-api-url \"https://graph.microsoft.com/beta/me\" --called-api-scopes \"user.read\"")]
//[InlineData("webapi\\webapi-b2c", "dotnet new webapi --auth IndividualB2C")]
//[InlineData("webapi\\webapi2-b2c-callswebapi", "dotnet new webapi --auth IndividualB2C --called-api-url \"https://fabrikamb2chello.azurewebsites.net/hello\" --called-api-scopes \"https://fabrikamb2c.onmicrosoft.com/helloapi/demo.read\"")]
//[InlineData("mvc\\mvc-noauth", "dotnet new mvc")]
//[InlineData("mvc\\mvc-singleorg", "dotnet new mvc --auth SingleOrg")]
//[InlineData("mvc\\mvc-singleorg-callsgraph", "dotnet new mvc --auth SingleOrg --calls-graph")]
//[InlineData("mvc\\mvc-singleorg-callswebapi", "dotnet new mvc --auth SingleOrg --called-api-url \"https://graph.microsoft.com/beta/me\" --called-api-scopes \"user.read\"")]
//[InlineData("mvc\\mvc-b2c", "dotnet new mvc --auth IndividualB2C")]
//[InlineData("mvc\\mvc-b2c-callswebapi", "dotnet new mvc --auth IndividualB2C --called-api-url \"https://fabrikamb2chello.azurewebsites.net/hello\" --called-api-scopes \"https://fabrikamb2c.onmicrosoft.com/helloapi/demo.read\"")]
//[InlineData("blazorserver\\blazorserver-noauth", "dotnet new blazorserver")]
//[InlineData("blazorserver\\blazorserver-singleorg", "dotnet new blazorserver --auth SingleOrg")]
//[InlineData("blazorserver\\blazorserver-singleorg-callsgraph", "dotnet new blazorserver --auth SingleOrg --calls-graph")]
//[InlineData("blazorserver\\blazorserver-singleorg-callswebapi", "dotnet new blazorserver --auth SingleOrg --called-api-url \"https://graph.microsoft.com/beta/me\" --called-api-scopes \"user.read\"")]
//[InlineData("blazorserver\\blazorserver-b2c", "dotnet new blazorserver --auth IndividualB2C")]
//[InlineData("blazorserver\\blazorserver-b2c-callswebapi", "dotnet new blazorserver --auth IndividualB2C --called-api-url \"https://fabrikamb2chello.azurewebsites.net/hello\" --called-api-scopes \"https://fabrikamb2c.onmicrosoft.com/helloapi/demo.read\"")]
//[InlineData("blazorwasm\\blazorwasm-noauth", "dotnet new blazorwasm")]
//[InlineData("blazorwasm\\blazorwasm-singleorg", "dotnet new blazorwasm --auth SingleOrg")]
//[InlineData("blazorwasm\\blazorwasm-singleorg-callsgraph", "dotnet new blazorwasm --auth SingleOrg --calls-graph")]
//[InlineData("blazorwasm\\blazorwasm-singleorg-callswebapi", "dotnet new blazorwasm --auth SingleOrg --called-api-url \"https://graph.microsoft.com/beta/me\" --called-api-scopes \"user.read\"")]
//[InlineData("blazorwasm\\blazorwasm-singleorg-hosted", "dotnet new blazorwasm --auth SingleOrg --hosted")]
//[InlineData("blazorwasm\\blazorwasm-singleorg-callsgraph-hosted", "dotnet new blazorwasm --auth SingleOrg --calls-graph --hosted")]
//[InlineData("blazorwasm\\blazorwasm-singleorg-callswebapi-hosted", "dotnet new blazorwasm --auth SingleOrg --called-api-url \"https://graph.microsoft.com/beta/me\" --called-api-scopes \"user.read\" --hosted")]
//[InlineData("blazorwasm\\blazorwasm-b2c", "dotnet new blazorwasm --auth IndividualB2C")]
//[InlineData("blazorwasm2\\blazorwasm2-b2c-hosted", "dotnet new blazorwasm --auth IndividualB2C --hosted")]
[Theory]
public async Task TestUpdateAppEndToEnd(string folder, string command)
{
// Create the folder
string executionFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
executionFolder = "C:\\temp\\test-provisionning";
string folderToCreate = Path.Combine(executionFolder, "Tests", folder);

// dotnet new command to create the project
RunProcess(command, folderToCreate, " --force");

// Add a secret if needed
if (command.Contains("--calls"))
{
try
{
RunProcess("dotnet user-secrets init", folderToCreate);
}
catch
{
// Silent catch
}
}

string currentDirectory = Directory.GetCurrentDirectory();

// Run the tool
try
{
Directory.SetCurrentDirectory(folderToCreate);

List<string> args = new List<string>();
args.Add("--tenant-id");
if (folder.Contains("b2c"))
{
args.Add("fabrikamb2c.onmicrosoft.com");
}
else
{
args.Add("testprovisionningtool.onmicrosoft.com");
}
await Program.Main(args.ToArray());
}
catch (Exception ex)
{
testOutput.WriteLine(ex.ToString());
Assert.True(false);
}
finally
{
Directory.SetCurrentDirectory(currentDirectory);
}

testOutput.WriteLine($"{folderToCreate}");
}
}
}

0 comments on commit 65c86a0

Please sign in to comment.