Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public AzureAdBlock(ApplicationParameters applicationParameters, JObject? existi
?? (applicationParameters.CallsDownstreamApi ? DefaultProperties.ApiScopes : applicationParameters.CallsMicrosoftGraph ? DefaultProperties.MicrosoftGraphScopes : null);
SignUpSignInPolicyId = !string.IsNullOrEmpty(applicationParameters.SusiPolicy) ? applicationParameters.SusiPolicy : existingBlock?.GetValue(PropertyNames.SignUpSignInPolicyId)?.ToString() ?? DefaultProperties.SignUpSignInPolicyId;
// TODO determine the SusiPolicy from the graph beta
Authority = IsB2C ? $"{Instance}{TenantId}/{SignUpSignInPolicyId}" : $"{Instance}{TenantId}";
Authority = IsB2C ? $"{Instance}{Domain}/{SignUpSignInPolicyId}" : $"{Instance}{Domain}";
ClientSecret = existingBlock?.GetValue(PropertyNames.ClientSecret)?.ToString() ?? DefaultProperties.ClientSecret;
ClientCertificates = existingBlock?.GetValue(PropertyNames.ClientCertificates)?.ToObject<string[]>();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public async Task AddAuthCodeAsync()
await HandleCodeFileAsync(file, project, options, codeModifierConfig.Identifier);
}

_consoleLogger.LogJsonMessage(new JsonResponse(Commands.UPDATE_PROJECT_COMMAND, State.Success, output: _output.ToString()));
_consoleLogger.LogJsonMessage(new JsonResponse(Commands.UPDATE_PROJECT_COMMAND, State.Success, output: _output.ToString().TrimEnd()));
}

private CodeModifierConfig? GetCodeModifierConfig()
Expand Down Expand Up @@ -148,26 +148,36 @@ private PropertyInfo? CodeModifierConfigPropertyInfo

private async Task HandleCodeFileAsync(CodeFile file, CodeAnalysis.Project project, CodeChangeOptions options, string identifier)
{
if (!string.IsNullOrEmpty(file.AddFilePath))
{
AddFile(file, identifier);
}
else
try
{
switch (file.Extension)
if (!string.IsNullOrEmpty(file.AddFilePath))
{
AddFile(file, identifier);
_output.AppendLine(string.Format(Resources.AddedCodeFile, file.AddFilePath));
}
else
{
case "cs":
await ModifyCsFile(file, project, options);
break;
case "cshtml":
await ModifyCshtmlFile(file, project, options);
break;
case "razor":
case "html":
await ApplyTextReplacements(file, project, options);
break;
switch (file.Extension)
{
case "cs":
await ModifyCsFile(file, project, options);
break;
case "cshtml":
await ModifyCshtmlFile(file, project, options);
break;
case "razor":
case "html":
await ApplyTextReplacements(file, project, options);
break;
}

_output.Append(string.Format(Resources.ModifiedCodeFile, file.FileName));
}
}
catch (Exception e)
{
_output.Append(string.Format(Resources.FailedToModifyCodeFile, file.FileName, e.Message));
}
}

/// <summary>
Expand All @@ -193,8 +203,6 @@ private void AddFile(CodeFile file, string identifier)
{
Directory.CreateDirectory(fileDir);
File.WriteAllText(filePath, codeFileString);
// TODO JsonResponse stringbuilder
_output.AppendLine($"Added {filePath}");
}
}

Expand Down Expand Up @@ -247,7 +255,6 @@ internal async Task ModifyCsFile(CodeFile file, CodeAnalysis.Project project, Co
{
documentEditor.ReplaceNode(documentEditor.OriginalRoot, modifiedRoot);
await documentBuilder.WriteToClassFileAsync(fileDoc.Name);
_output.AppendLine($"Modified {file.FileName}"); // TODO strings.
}
}

Expand All @@ -258,7 +265,7 @@ internal async Task ModifyCsFile(CodeFile file, CodeAnalysis.Project project, Co
/// <param name="options"></param>
/// <param name="file"></param>
/// <returns>modified root if there are changes, else null</returns>
private static SyntaxNode? ModifyRoot(DocumentBuilder documentBuilder, CodeChangeOptions options, CodeFile file)
private SyntaxNode? ModifyRoot(DocumentBuilder documentBuilder, CodeChangeOptions options, CodeFile file)
{
var root = documentBuilder.AddUsings(options);
if (file.FileName.Equals("Program.cs") && file.Methods.TryGetValue("Global", out var globalChanges))
Expand All @@ -275,16 +282,15 @@ internal async Task ModifyCsFile(CodeFile file, CodeAnalysis.Project project, Co
var mainMethod = root?.DescendantNodes().OfType<MethodDeclarationSyntax>()
.FirstOrDefault(n => Main.Equals(n.Identifier.ToString(), StringComparison.OrdinalIgnoreCase));
if (mainMethod != null
&& DocumentBuilder.ApplyChangesToMethod(mainMethod.Body, filteredChanges) is BlockSyntax updatedBody)
&& DocumentBuilder.ApplyChangesToMethod(mainMethod.Body, filteredChanges, file.FileName, _output) is BlockSyntax updatedBody)
{
var updatedMethod = mainMethod.WithBody(updatedBody);
return root?.ReplaceNode(mainMethod, updatedMethod);
}

}
else if (root.Members.Any(node => node.IsKind(SyntaxKind.GlobalStatement)))
{
return DocumentBuilder.ApplyChangesToMethod(root, filteredChanges);
return DocumentBuilder.ApplyChangesToMethod(root, filteredChanges, file.FileName, _output);
}
}
else
Expand All @@ -305,7 +311,7 @@ node is ClassDeclarationSyntax cds &&
//add class attributes
modifiedClassDeclarationSyntax = documentBuilder.AddClassAttributes(modifiedClassDeclarationSyntax, options);
//add code snippets/changes.
modifiedClassDeclarationSyntax = ModifyMethods(modifiedClassDeclarationSyntax, documentBuilder, file.Methods, options);
modifiedClassDeclarationSyntax = ModifyMethods(file.FileName, modifiedClassDeclarationSyntax, documentBuilder, file.Methods, options, _output);

//replace class node with all the updates.
#pragma warning disable CS8631 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match constraint type.
Expand All @@ -317,7 +323,7 @@ node is ClassDeclarationSyntax cds &&
return root;
}

private static ClassDeclarationSyntax ModifyMethods(ClassDeclarationSyntax classNode, DocumentBuilder documentBuilder, Dictionary<string, Method> methods, CodeChangeOptions options)
private static ClassDeclarationSyntax ModifyMethods(string fileName, ClassDeclarationSyntax classNode, DocumentBuilder documentBuilder, Dictionary<string, Method> methods, CodeChangeOptions options, StringBuilder output)
{
foreach ((string methodName, Method methodChanges) in methods)
{
Expand All @@ -338,7 +344,7 @@ private static ClassDeclarationSyntax ModifyMethods(ClassDeclarationSyntax class
methodChanges.CodeChanges = ProjectModifierHelper.UpdateVariables(methodChanges.CodeChanges, oldValue, newValue);
}

var updatedMethodNode = DocumentBuilder.GetModifiedMethod(methodNode, methodChanges, options);
var updatedMethodNode = DocumentBuilder.GetModifiedMethod(fileName, methodNode, methodChanges, options, output);
if (updatedMethodNode != null)
{
classNode = classNode.ReplaceNode(methodNode, updatedMethodNode);
Expand Down Expand Up @@ -366,9 +372,7 @@ internal async Task ModifyCshtmlFile(CodeFile file, CodeAnalysis.Project project
var editedDocument = await ProjectModifierHelper.ModifyDocumentText(fileDoc, filteredCodeChanges);
if (editedDocument != null)
{
//replace the document
var output = await ProjectModifierHelper.UpdateDocument(editedDocument);
_output.AppendLine(output);
await ProjectModifierHelper.UpdateDocument(editedDocument);
}
}

Expand Down Expand Up @@ -396,8 +400,7 @@ internal async Task ApplyTextReplacements(CodeFile file, CodeAnalysis.Project pr
var editedDocument = await ProjectModifierHelper.ModifyDocumentText(document, replacements);
if (editedDocument != null)
{
var output = await ProjectModifierHelper.UpdateDocument(editedDocument);
_output.AppendLine(output);
await ProjectModifierHelper.UpdateDocument(editedDocument);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Azure.Core;
using Microsoft.DotNet.MSIdentity.AuthenticationParameters;
Expand All @@ -16,12 +17,11 @@ namespace Microsoft.DotNet.MSIdentity.MicrosoftIdentityPlatformApplication
{
public class MicrosoftIdentityPlatformApplicationManager
{
private StringBuilder _output = new StringBuilder();
const string MicrosoftGraphAppId = "00000003-0000-0000-c000-000000000000";
const string ScopeType = "Scope";

private const string DefaultCallbackPath = "signin-oidc";
private const string BlazorWasmCallbackPath = "authentication/login-callback";

GraphServiceClient? _graphServiceClient;

internal async Task<ApplicationParameters?> CreateNewAppAsync(
Expand Down Expand Up @@ -74,15 +74,21 @@ public class MicrosoftIdentityPlatformApplicationManager
AppId = createdApplication.AppId,
};

await graphServiceClient.ServicePrincipals
ServicePrincipal? createdSp = await graphServiceClient.ServicePrincipals
.Request()
.AddAsync(servicePrincipal).ConfigureAwait(false);
.AddAsync(servicePrincipal);

if (createdSp is null)
{
consoleLogger.LogJsonMessage(new JsonResponse(commandName, State.Fail, output: Resources.FailedToGetServicePrincipal));
return null;
}

// B2C does not allow user consent, and therefore we need to explicity grant permissions
if (applicationParameters.IsB2C)
{
string scopes = GetMsGraphScopes(applicationParameters); // Explicit usage of MicrosoftGraph openid and offline_access in the case of Azure AD B2C.
await AddDownstreamApiPermissions(createdApplication.AppId, scopes, graphServiceClient, application);
await AddDownstreamApiPermissions(scopes, graphServiceClient, application, createdSp);
}

// For web API, we need to know the appId of the created app to compute the Identifier URI,
Expand Down Expand Up @@ -205,12 +211,18 @@ internal async Task<JsonResponse> UpdateApplication(
}

(bool needsUpdates, Application appUpdates) = GetApplicationUpdates(remoteApp, toolOptions, parameters);

StringBuilder output = new StringBuilder();
// B2C does not allow user consent, and therefore we need to explicity grant permissions
if (parameters.IsB2C && parameters.CallsDownstreamApi)
if (parameters.IsB2C && parameters.CallsDownstreamApi && !string.IsNullOrEmpty(toolOptions.ApiScopes))
{
// TODO: Add if it's B2C, acquire or create the SUSI Policy
await AddDownstreamApiPermissions(parameters.ClientId, toolOptions.ApiScopes, graphServiceClient, appUpdates);
var servicePrincipal = await GetOrCreateSP(graphServiceClient, parameters.ClientId);
if (servicePrincipal is null)
{
return new JsonResponse(commandName, State.Fail, output: Resources.FailedToGetServicePrincipal);
}

await AddDownstreamApiPermissions(toolOptions.ApiScopes, graphServiceClient, appUpdates, servicePrincipal, output);
needsUpdates = true;
}

Expand All @@ -223,40 +235,37 @@ internal async Task<JsonResponse> UpdateApplication(
{
// TODO: update other fields, see https://github.com/jmprieur/app-provisonning-tool/issues/10
var updatedApp = await graphServiceClient.Applications[remoteApp.Id].Request().UpdateAsync(appUpdates);
return new JsonResponse(commandName, State.Success, output: string.Format(Resources.SuccessfullyUpdatedApp, remoteApp.DisplayName, remoteApp.AppId));
output.Append(string.Format(Resources.SuccessfullyUpdatedApp, remoteApp.DisplayName));
return new JsonResponse(commandName, State.Success, output.ToString(), remoteApp.AppId);
}
catch (ServiceException se)
{
return new JsonResponse(commandName, State.Fail, output: se.Error?.Message);
output.Append(se.Error?.Message);
return new JsonResponse(commandName, State.Fail, output.ToString());
}
}

internal static async Task AddDownstreamApiPermissions(string? clientId, string? apiScopes, GraphServiceClient graphServiceClient, Application appUpdates)
internal static async Task AddDownstreamApiPermissions(string? apiScopes, GraphServiceClient graphServiceClient, Application appUpdates, ServicePrincipal servicePrincipal, StringBuilder? output = null)
{
IEnumerable<IGrouping<string, ResourceAndScope>>? scopesPerResource = await AddApiPermissions(
apiScopes,
graphServiceClient,
appUpdates).ConfigureAwait(false);

// TODO need to have admin permissions for the downstream API
ServicePrincipal? servicePrincipal = await GetOrCreateSP(graphServiceClient, clientId);
if (servicePrincipal == null)
{
throw new ArgumentNullException(nameof(servicePrincipal));
}

await AddAdminConsentToApiPermissions(
graphServiceClient,
servicePrincipal,
scopesPerResource);
scopesPerResource,
output);
}

private static async Task<ServicePrincipal?> GetOrCreateSP(GraphServiceClient graphServiceClient, string? clientId)
{
var servicePrincipal = (await graphServiceClient.ServicePrincipals
.Request()
.Filter($"appId eq '{clientId}'")
.GetAsync()).FirstOrDefault();
.GetAsync())?.FirstOrDefault();

if (servicePrincipal is null)
{
Expand Down Expand Up @@ -527,7 +536,7 @@ await graphServiceClient.Applications[graphEntityId]
internal static async Task ExposeWebApiScopes(GraphServiceClient graphServiceClient, Application createdApplication, ApplicationParameters applicationParameters)
{
var scopes = createdApplication.Api.Oauth2PermissionScopes?.ToList() ?? new List<PermissionScope>();
var scopeName = applicationParameters.IsB2C ? $"https://{createdApplication.PublisherDomain}/{createdApplication.AppId}" : $"api://{createdApplication.Id}";
var scopeName = applicationParameters.IsB2C ? $"https://{createdApplication.PublisherDomain}/{createdApplication.AppId}" : $"api://{createdApplication.AppId}";
await ExposeScopes(graphServiceClient, scopeName, createdApplication.Id, scopes);
}

Expand All @@ -540,7 +549,8 @@ internal static async Task ExposeWebApiScopes(GraphServiceClient graphServiceCli
private static async Task AddAdminConsentToApiPermissions(
GraphServiceClient graphServiceClient,
ServicePrincipal servicePrincipal,
IEnumerable<IGrouping<string, ResourceAndScope>>? scopesPerResource)
IEnumerable<IGrouping<string, ResourceAndScope>>? scopesPerResource,
StringBuilder? output = null)
{
// Consent to the scopes
if (scopesPerResource != null)
Expand All @@ -558,18 +568,20 @@ private static async Task AddAdminConsentToApiPermissions(
Scope = string.Join(" ", resourceAndScopes.Select(r => r.Scope))
};

// TODO: check if the permissions are already there
var existingPermissionGrants = (await graphServiceClient.Oauth2PermissionGrants
.Request()
.GetAsync());
if (!existingPermissionGrants.ToArray().Any(p => p.Equals(oAuth2PermissionGrant)))
// Check if permissions already exist, otherwise will throw exception

try
{
// 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);
}
catch (Microsoft.Graph.ServiceException ex)
{
output?.AppendLine(ex.Message);
}
}
}
}
Expand Down
Loading