diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/AuthenticationParameters/AzureAdProperties.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/AuthenticationParameters/AzureAdProperties.cs index 55a6e86a9..e758d59ba 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/AuthenticationParameters/AzureAdProperties.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/AuthenticationParameters/AzureAdProperties.cs @@ -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(); } diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/ProjectModifier.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/ProjectModifier.cs index 71ab9562a..c118ec224 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/ProjectModifier.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/CodeReaderWriter/ProjectModifier.cs @@ -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() @@ -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)); + } } /// @@ -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}"); } } @@ -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. } } @@ -258,7 +265,7 @@ internal async Task ModifyCsFile(CodeFile file, CodeAnalysis.Project project, Co /// /// /// modified root if there are changes, else null - 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)) @@ -275,16 +282,15 @@ internal async Task ModifyCsFile(CodeFile file, CodeAnalysis.Project project, Co var mainMethod = root?.DescendantNodes().OfType() .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 @@ -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. @@ -317,7 +323,7 @@ node is ClassDeclarationSyntax cds && return root; } - private static ClassDeclarationSyntax ModifyMethods(ClassDeclarationSyntax classNode, DocumentBuilder documentBuilder, Dictionary methods, CodeChangeOptions options) + private static ClassDeclarationSyntax ModifyMethods(string fileName, ClassDeclarationSyntax classNode, DocumentBuilder documentBuilder, Dictionary methods, CodeChangeOptions options, StringBuilder output) { foreach ((string methodName, Method methodChanges) in methods) { @@ -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); @@ -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); } } @@ -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); } } } diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/MicrosoftIdentityPlatformApplicationManager.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/MicrosoftIdentityPlatformApplicationManager.cs index 35e002adf..05038a087 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/MicrosoftIdentityPlatformApplicationManager.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/MicrosoftIdentityPlatform/MicrosoftIdentityPlatformApplicationManager.cs @@ -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; @@ -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 CreateNewAppAsync( @@ -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, @@ -205,12 +211,18 @@ internal async Task 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; } @@ -223,15 +235,17 @@ internal async Task 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>? scopesPerResource = await AddApiPermissions( apiScopes, @@ -239,16 +253,11 @@ internal static async Task AddDownstreamApiPermissions(string? clientId, string? 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 GetOrCreateSP(GraphServiceClient graphServiceClient, string? clientId) @@ -256,7 +265,7 @@ await AddAdminConsentToApiPermissions( var servicePrincipal = (await graphServiceClient.ServicePrincipals .Request() .Filter($"appId eq '{clientId}'") - .GetAsync()).FirstOrDefault(); + .GetAsync())?.FirstOrDefault(); if (servicePrincipal is null) { @@ -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(); - 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); } @@ -540,7 +549,8 @@ internal static async Task ExposeWebApiScopes(GraphServiceClient graphServiceCli private static async Task AddAdminConsentToApiPermissions( GraphServiceClient graphServiceClient, ServicePrincipal servicePrincipal, - IEnumerable>? scopesPerResource) + IEnumerable>? scopesPerResource, + StringBuilder? output = null) { // Consent to the scopes if (scopesPerResource != null) @@ -558,11 +568,9 @@ 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 @@ -570,6 +578,10 @@ await graphServiceClient.Oauth2PermissionGrants .Request() .AddAsync(oAuth2PermissionGrant); } + catch (Microsoft.Graph.ServiceException ex) + { + output?.AppendLine(ex.Message); + } } } } diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.Designer.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.Designer.cs index c107132f7..30f7f7b58 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.Designer.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.Designer.cs @@ -253,6 +253,15 @@ internal static string add_dotnet_blazorwasm_UserProfile_razor { } } + /// + /// Looks up a localized string similar to Added code file {0}. + /// + internal static string AddedCodeFile { + get { + return ResourceManager.GetString("AddedCodeFile", resourceCulture); + } + } + /// /// Looks up a localized string similar to Added {0} to user secrets.. /// @@ -547,6 +556,24 @@ internal static string FailedToCreateApp { } } + /// + /// Looks up a localized string similar to Failed to get or create service principal.. + /// + internal static string FailedToGetServicePrincipal { + get { + return ResourceManager.GetString("FailedToGetServicePrincipal", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to modify code file {0}, {1} . + /// + internal static string FailedToModifyCodeFile { + get { + return ResourceManager.GetString("FailedToModifyCodeFile", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to retrieve all Azure AD/AD B2C objects(apps/service principals. /// @@ -601,6 +628,15 @@ internal static string MismatchedProjectTypeIdentifier { } } + /// + /// Looks up a localized string similar to Modified code file {0}. + /// + internal static string ModifiedCodeFile { + get { + return ResourceManager.GetString("ModifiedCodeFile", resourceCulture); + } + } + /// /// Looks up a localized string similar to No valid project description found with project type identifier "{0}". /// diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.resx b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.resx index feabfa321..6b9559ae3 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.resx +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Properties/Resources.resx @@ -117,6 +117,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Added code file {0} + 0 = File name + Added {0} to user secrets. @@ -239,6 +243,13 @@ Failed to create Azure AD/AD B2C app registration. + + Failed to get or create service principal. + + + Failed to modify code file {0}, {1} + 0 = File name, 1 = Exception message + Failed to retrieve all Azure AD/AD B2C objects(apps/service principals @@ -260,6 +271,10 @@ Config identifier: {0} does not match toolOptions identifier: {1} 0 = codeModifierConfig.Identifier, 1 = _toolOptions.ProjectTypeIdentifier + + Modified code file {0} + 0 = File name + No valid project description found with project type identifier "{0}" 0 = ProjectTypeIdentifier diff --git a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/AppProvisioningTool.cs b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/AppProvisioningTool.cs index eba909695..40c6d37cc 100644 --- a/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/AppProvisioningTool.cs +++ b/src/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity/Tool/AppProvisioningTool.cs @@ -287,7 +287,6 @@ private async Task UpdateAppRegistration(TokenCredential tokenCredential, Applic if (string.IsNullOrEmpty(applicationParameters.AppIdUri)) // Expose server API scopes { var graphServiceClient = MicrosoftIdentityPlatformApplicationManager.GetGraphServiceClient(tokenCredential); - // TODO test with B2C applicationParameters.AppIdUri = $"api://{applicationParameters.ClientId}"; await MicrosoftIdentityPlatformApplicationManager.ExposeScopes(graphServiceClient, applicationParameters.AppIdUri, applicationParameters.GraphEntityId); } @@ -304,8 +303,8 @@ private async Task UpdateAppRegistration(TokenCredential tokenCredential, Applic CommandName); output.Append(jsonResponse.Output); - var response = new JsonResponse(CommandName, jsonResponse.State, output: output.ToString()); + ConsoleLogger.LogJsonMessage(new JsonResponse(CommandName, jsonResponse.State, output: output.ToString())); } /// diff --git a/src/Shared/Microsoft.DotNet.Scaffolding.Shared/CodeModifier/DocumentBuilder.cs b/src/Shared/Microsoft.DotNet.Scaffolding.Shared/CodeModifier/DocumentBuilder.cs index 8f8903369..a6c6ce2a4 100644 --- a/src/Shared/Microsoft.DotNet.Scaffolding.Shared/CodeModifier/DocumentBuilder.cs +++ b/src/Shared/Microsoft.DotNet.Scaffolding.Shared/CodeModifier/DocumentBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -11,6 +12,7 @@ using Microsoft.DotNet.MSIdentity.Shared; using Microsoft.DotNet.Scaffolding.Shared.CodeModifier.CodeChange; using Microsoft.DotNet.Scaffolding.Shared.Project; +using NuGet.Protocol; namespace Microsoft.DotNet.Scaffolding.Shared.CodeModifier { @@ -32,9 +34,9 @@ public DocumentBuilder( _docRoot = (CompilationUnitSyntax)_documentEditor.OriginalRoot ?? throw new ArgumentNullException(nameof(_documentEditor.OriginalRoot)); } - internal static BaseMethodDeclarationSyntax GetModifiedMethod(BaseMethodDeclarationSyntax method, Method methodChanges, CodeChangeOptions options) + internal static BaseMethodDeclarationSyntax GetModifiedMethod(string fileName, BaseMethodDeclarationSyntax method, Method methodChanges, CodeChangeOptions options, StringBuilder output) { - method = AddCodeSnippetsToMethod(method, methodChanges, options); + method = AddCodeSnippetsToMethod(fileName, method, methodChanges, options, output); method = EditMethodReturnType(method, methodChanges, options); method = AddMethodParameters(method, methodChanges, options); return method; @@ -133,7 +135,6 @@ public async Task WriteToClassFileAsync(string filePath) var changedDocument = GetDocument(); var classFileTxt = await changedDocument.GetTextAsync(); File.WriteAllText(filePath, classFileTxt.ToString()); - _consoleLogger.LogMessage($"Modified {filePath}.\n"); } internal static BaseMethodDeclarationSyntax AddMethodParameters(BaseMethodDeclarationSyntax originalMethod, Method methodChanges, CodeChangeOptions options) @@ -150,7 +151,7 @@ internal static BaseMethodDeclarationSyntax AddMethodParameters(BaseMethodDeclar } // Add all the different code snippet. - internal static BaseMethodDeclarationSyntax AddCodeSnippetsToMethod(BaseMethodDeclarationSyntax originalMethod, Method methodChanges, CodeChangeOptions options) + internal static BaseMethodDeclarationSyntax AddCodeSnippetsToMethod(string fileName, BaseMethodDeclarationSyntax originalMethod, Method methodChanges, CodeChangeOptions options, StringBuilder output) { var filteredChanges = ProjectModifierHelper.FilterCodeSnippets(methodChanges.CodeChanges, options); @@ -161,22 +162,33 @@ internal static BaseMethodDeclarationSyntax AddCodeSnippetsToMethod(BaseMethodDe var blockSyntax = originalMethod.Body; - var modifiedMethod = ApplyChangesToMethod(blockSyntax, filteredChanges); + var modifiedMethod = ApplyChangesToMethod(blockSyntax, filteredChanges, fileName, output); return originalMethod.ReplaceNode(blockSyntax, modifiedMethod); } - internal static SyntaxNode ApplyChangesToMethod(SyntaxNode root, CodeSnippet[] filteredChanges) + internal static SyntaxNode ApplyChangesToMethod(SyntaxNode root, CodeSnippet[] filteredChanges, string fileName = null, StringBuilder output = null) { + bool changesMade = false; foreach (var change in filteredChanges) { - var update = ModifyMethod(root, change); + var update = ModifyMethod(root, change, output); if (update != null) { + changesMade = true; root = root.ReplaceNode(root, update); } } + if (!changesMade) + { + output?.AppendLine(value: $"No modifications made for file: {fileName}"); + } + else + { + output?.AppendLine($"Modified {fileName}"); // TODO strings. + } + return root; } @@ -213,26 +225,33 @@ internal static CodeSnippet[] AddLeadingTriviaSpaces(CodeSnippet[] snippets, int return snippets; } - internal static SyntaxNode ModifyMethod(SyntaxNode originalMethod, CodeSnippet codeChange) + internal static SyntaxNode ModifyMethod(SyntaxNode originalMethod, CodeSnippet codeChange, StringBuilder output = null) { - SyntaxNode modifiedMethod; - switch (codeChange.CodeChangeType) + SyntaxNode modifiedMethod = null; + try { - case CodeChangeType.Lambda: - { - modifiedMethod = AddOrUpdateLambda(originalMethod, codeChange); - break; - } - case CodeChangeType.MemberAccess: - { - modifiedMethod = AddExpressionToParent(originalMethod, codeChange); - break; - } - default: - { - modifiedMethod = UpdateMethod(originalMethod, codeChange); - break; - } + switch (codeChange.CodeChangeType) + { + case CodeChangeType.Lambda: + { + modifiedMethod = AddOrUpdateLambda(originalMethod, codeChange); + break; + } + case CodeChangeType.MemberAccess: + { + modifiedMethod = AddExpressionToParent(originalMethod, codeChange); + break; + } + default: + { + modifiedMethod = UpdateMethod(originalMethod, codeChange); + break; + } + } + } + catch + { + output?.Append(value: $"Error modifying method {originalMethod}\nCodeChange:{codeChange.ToJson()}"); } return modifiedMethod != null ? originalMethod.ReplaceNode(originalMethod, modifiedMethod) : originalMethod; @@ -241,6 +260,11 @@ internal static SyntaxNode ModifyMethod(SyntaxNode originalMethod, CodeSnippet c internal static SyntaxNode UpdateMethod(SyntaxNode originalMethod, CodeSnippet codeChange) { var children = GetDescendantNodes(originalMethod); + if (children is null) + { + return originalMethod; + } + //check for CodeChange.Block and CodeChange.CheckBlock for block's are easy to check. if (ProjectModifierHelper.StatementExists(children, codeChange.Block) || (!string.IsNullOrEmpty(codeChange.CheckBlock) && ProjectModifierHelper.StatementExists(children, codeChange.CheckBlock))) { @@ -261,7 +285,6 @@ internal static SyntaxNode UpdateMethod(SyntaxNode originalMethod, CodeSnippet c updatedMethod = GetBlockStatement(originalMethod, codeChange); } - return updatedMethod ?? originalMethod; } @@ -429,9 +452,10 @@ internal static SyntaxNode GetSpecifiedNode(string specifierStatement, IEnumerab return null; } + // TODO descendantNodes could be null var specifiedDescendant = - descendantNodes.FirstOrDefault(d => d != null && d.ToString().Contains(specifierStatement)) ?? - descendantNodes.FirstOrDefault(d => d != null && d.ToString().Contains(ProjectModifierHelper.TrimStatement(specifierStatement))); + descendantNodes?.FirstOrDefault(d => d != null && d.ToString().Contains(specifierStatement)) ?? + descendantNodes?.FirstOrDefault(d => d != null && d.ToString().Contains(ProjectModifierHelper.TrimStatement(specifierStatement))); return specifiedDescendant; } @@ -492,17 +516,25 @@ internal static BaseMethodDeclarationSyntax AddParameters(BaseMethodDeclarationS private static SyntaxNode AddOrUpdateLambda(SyntaxNode originalMethod, CodeSnippet change) { - var rootDescendants = GetDescendantNodes(originalMethod); - var parent = GetSpecifiedNode(change.Parent, rootDescendants); + var descendants = GetDescendantNodes(originalMethod); + if (descendants is null) + { + return originalMethod; + } + + var parent = GetSpecifiedNode(change.Parent, descendants); if (parent is null) { return originalMethod; } var children = GetDescendantNodes(parent); + if (children is null) + { + return originalMethod; + } var updatedParent = parent; - // Check for existing lambda if (children.FirstOrDefault( d => d.IsKind(SyntaxKind.ParenthesizedLambdaExpression) @@ -535,7 +567,12 @@ internal static SyntaxNode GetNodeWithUpdatedLambda(LambdaExpressionSyntax exist private static LambdaExpressionSyntax UpdateLambdaParameters(LambdaExpressionSyntax existingLambda, CodeSnippet change) { - var existingParameters = GetDescendantNodes(existingLambda).Where(n => n.IsKind(SyntaxKind.Parameter)); + var existingParameters = GetDescendantNodes(existingLambda)?.Where(n => n.IsKind(SyntaxKind.Parameter)); + if (existingParameters is null) + { + return existingLambda; + } + if (ProjectModifierHelper.StatementExists(existingParameters, change.Parameter)) { return existingLambda; @@ -644,13 +681,24 @@ internal static SyntaxNode AddLambdaToParent(SyntaxNode parent, IEnumerable GetDescendantNodes(SyntaxNode root) return compilationUnit.Members; } - return root.DescendantNodes(); + return root?.DescendantNodes(); } // create UsingDirectiveSyntax[] using a string[] to add to the root of the class (root.Usings). diff --git a/src/Shared/Microsoft.DotNet.Scaffolding.Shared/Project/ProjectModifierHelper.cs b/src/Shared/Microsoft.DotNet.Scaffolding.Shared/Project/ProjectModifierHelper.cs index 7ce8ded0d..b06296751 100644 --- a/src/Shared/Microsoft.DotNet.Scaffolding.Shared/Project/ProjectModifierHelper.cs +++ b/src/Shared/Microsoft.DotNet.Scaffolding.Shared/Project/ProjectModifierHelper.cs @@ -493,12 +493,10 @@ internal static async Task ModifyDocumentText(Document fileDoc, IEnume return fileDoc.WithText(updatedSourceText); } - internal static async Task UpdateDocument(Document document) + internal static async Task UpdateDocument(Document document) { var classFileTxt = await document.GetTextAsync(); File.WriteAllText(document.Name, classFileTxt.ToString(), new UTF8Encoding(false)); - - return $"Modified {document.Name}.\n"; // todo strings. } // Filter out CodeBlocks that are invalid using FilterOptions diff --git a/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.UnitTests.Tests/AppProvisioningToolTests.cs b/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.UnitTests.Tests/AppProvisioningToolTests.cs index 443c3e55a..869d5d4bf 100644 --- a/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.UnitTests.Tests/AppProvisioningToolTests.cs +++ b/test/MSIdentityScaffolding/Microsoft.DotNet.MSIdentity.UnitTests.Tests/AppProvisioningToolTests.cs @@ -75,7 +75,7 @@ public void ModifyAppSettings_NoInput_Blazor_DefaultOutput() var expected = JObject.FromObject(new { - Authority = $"{DefaultProperties.Instance}{DefaultProperties.TenantId}", + Authority = $"{DefaultProperties.Instance}{DefaultProperties.Domain}", DefaultProperties.ClientId, DefaultProperties.ValidateAuthority }); @@ -93,7 +93,7 @@ public void ModifyAppSettings_NoInput_BlazorB2C_DefaultOutput() var expected = JObject.FromObject(new { - Authority = $"{DefaultProperties.Instance}{DefaultProperties.TenantId}/{DefaultProperties.SignUpSignInPolicyId}", + Authority = $"{DefaultProperties.Instance}{DefaultProperties.Domain}/{DefaultProperties.SignUpSignInPolicyId}", DefaultProperties.ClientId, ValidateAuthority = false }); @@ -532,7 +532,7 @@ public void ModifyAppSettings_BlazorWasm_AuthorityIsCorrect() var expected = JObject.FromObject(new { ClientId = inputClientId, - Authority = $"{inputInstance}{inputTenantId}", + Authority = $"{inputInstance}{inputDomain}", ValidateAuthority = true }); @@ -573,7 +573,7 @@ public void ModifyAppSettings_BlazorWasmB2C_AuthorityIsCorrect() var expected = JObject.FromObject(new { ClientId = inputClientId, - Authority = $"{inputInstance}{inputTenantId}/{existingSusi}", + Authority = $"{inputInstance}{inputDomain}/{existingSusi}", ValidateAuthority = false }); diff --git a/test/Shared/Microsoft.DotNet.Scaffolding.Shared.Tests/DocumentBuilderTests.cs b/test/Shared/Microsoft.DotNet.Scaffolding.Shared.Tests/DocumentBuilderTests.cs index d0bac859e..0c1a11f15 100644 --- a/test/Shared/Microsoft.DotNet.Scaffolding.Shared.Tests/DocumentBuilderTests.cs +++ b/test/Shared/Microsoft.DotNet.Scaffolding.Shared.Tests/DocumentBuilderTests.cs @@ -268,7 +268,7 @@ public async Task AddGlobalStatementsTests(string[] statementsToAdd, string[] du Document minimalProgramCsDoc = CreateDocument(MinimalProgramCsFile); var root = await minimalProgramCsDoc.GetSyntaxRootAsync() as CompilationUnitSyntax; var codeChanges = statementsToAdd.Select(s => new CodeSnippet { Block = s }).ToArray(); - var modifiedRoot = DocumentBuilder.ApplyChangesToMethod(root, codeChanges) as CompilationUnitSyntax; + var modifiedRoot = DocumentBuilder.ApplyChangesToMethod(root, codeChanges, "filename") as CompilationUnitSyntax; foreach (var statementToAdd in statementsToAdd) { @@ -278,7 +278,7 @@ public async Task AddGlobalStatementsTests(string[] statementsToAdd, string[] du } var duplicates = duplicateStatements.Select(s => new CodeSnippet { Block = s }).ToArray(); - var rootWithDuplicates = DocumentBuilder.ApplyChangesToMethod(modifiedRoot, duplicates) as CompilationUnitSyntax; + var rootWithDuplicates = DocumentBuilder.ApplyChangesToMethod(modifiedRoot, duplicates, "filename") as CompilationUnitSyntax; Assert.Equal(rootWithDuplicates.Members.Count, modifiedRoot.Members.Count); }