From dc7535b149e55a0540db472cfaff15cf705545a4 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sat, 2 Sep 2017 23:37:55 +0200 Subject: [PATCH] 1) Email claim for API 2) Update content form after edit. --- .../Scripting/ScriptUser.cs | 29 ++++++- .../Contents/Commands/ContentCommand.cs | 2 +- .../Contents/ContentCommandMiddleware.cs | 2 +- .../UserClaimsPrincipalFactoryWithEmail.cs | 36 +++++++++ .../Config/Identity/IdentityServices.cs | 6 +- .../Config/Identity/LazyClientStore.cs | 1 + .../ContentApi/ContentsController.cs | 16 ++-- .../pages/content/content-page.component.ts | 1 + .../app/shared/services/auth.service.ts | 2 +- .../Scripting/ScriptUserTests.cs | 80 +++++++++++++++++++ .../Contents/ContentCommandHandlerTests.cs | 24 +++--- 11 files changed, 172 insertions(+), 27 deletions(-) create mode 100644 src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs create mode 100644 tests/Squidex.Domain.Apps.Core.Tests/Scripting/ScriptUserTests.cs diff --git a/src/Squidex.Domain.Apps.Core/Scripting/ScriptUser.cs b/src/Squidex.Domain.Apps.Core/Scripting/ScriptUser.cs index d799f7ff9..8be73a193 100644 --- a/src/Squidex.Domain.Apps.Core/Scripting/ScriptUser.cs +++ b/src/Squidex.Domain.Apps.Core/Scripting/ScriptUser.cs @@ -7,23 +7,44 @@ // ========================================================================== using System.Collections.Generic; +using System.Linq; using System.Security.Claims; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Security; +// ReSharper disable ConvertIfStatementToConditionalTernaryExpression namespace Squidex.Domain.Apps.Core.Scripting { public sealed class ScriptUser { + public bool IsClient { get; set; } + public string Id { get; set; } public string Email { get; set; } - public bool IsClient { get; set; } - - public Dictionary Scopes { get; } = new Dictionary(); + public Dictionary Claims { get; set; } public static ScriptUser Create(ClaimsPrincipal principal) { - return new ScriptUser(); + Guard.NotNull(principal, nameof(principal)); + + var subjectId = principal.OpenIdSubject(); + + var user = new ScriptUser { IsClient = string.IsNullOrWhiteSpace(subjectId), Email = principal.OpenIdEmail() }; + + if (!user.IsClient) + { + user.Id = subjectId; + } + else + { + user.Id = principal.OpenIdClientId(); + } + + user.Claims = principal.Claims.GroupBy(x => x.Type).ToDictionary(x => x.Key, x => x.Select(y => y.Value).ToArray()); + + return user; } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentCommand.cs b/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentCommand.cs index 67ec680ed..55be9acc8 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentCommand.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/Commands/ContentCommand.cs @@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Write.Contents.Commands { public abstract class ContentCommand : SchemaCommand, IAggregateCommand { - public ClaimsPrincipal Principal { get; set; } + public ClaimsPrincipal User { get; set; } public Guid ContentId { get; set; } diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs index 09fcdde31..f376b5ffc 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs @@ -196,7 +196,7 @@ namespace Squidex.Domain.Apps.Write.Contents private static ScriptContext CreateScriptContext(ContentDomainObject content, ContentCommand command, NamedContentData data = null) { - return new ScriptContext { ContentId = content.Id, Data = data, OldData = content.Data, User = ScriptUser.Create(command.Principal) }; + return new ScriptContext { ContentId = content.Id, Data = data, OldData = content.Data, User = ScriptUser.Create(command.User) }; } private async Task<(ISchemaEntity SchemaEntity, IAppEntity AppEntity)> ResolveSchemaAndAppAsync(SchemaCommand command) diff --git a/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs b/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs new file mode 100644 index 000000000..a326b264e --- /dev/null +++ b/src/Squidex.Domain.Users/UserClaimsPrincipalFactoryWithEmail.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// UserClaimsPrincipalFactoryWithEmail.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Squidex.Infrastructure.Security; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Users +{ + public sealed class UserClaimsPrincipalFactoryWithEmail : UserClaimsPrincipalFactory + { + public UserClaimsPrincipalFactoryWithEmail(UserManager userManager, RoleManager roleManager, IOptions optionsAccessor) + : base(userManager, roleManager, optionsAccessor) + { + } + + public override async Task CreateAsync(IUser user) + { + var principal = await base.CreateAsync(user); + + principal.Identities.First().AddClaim(new Claim(OpenIdClaims.Email, await UserManager.GetEmailAsync(user))); + + return principal; + } + } +} diff --git a/src/Squidex/Config/Identity/IdentityServices.cs b/src/Squidex/Config/Identity/IdentityServices.cs index 013e9bc3e..5437166e8 100644 --- a/src/Squidex/Config/Identity/IdentityServices.cs +++ b/src/Squidex/Config/Identity/IdentityServices.cs @@ -15,6 +15,7 @@ using IdentityModel; using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Users; @@ -91,6 +92,8 @@ namespace Squidex.Config.Identity GetApiResources()); services.AddSingleton( GetIdentityResources()); + services.AddSingleton, + UserClaimsPrincipalFactoryWithEmail>(); services.AddSingleton(); services.AddSingleton { + JwtClaimTypes.Email, JwtClaimTypes.Role } }; @@ -130,7 +134,7 @@ namespace Squidex.Config.Identity { yield return new IdentityResources.OpenId(); yield return new IdentityResources.Profile(); - yield return new IdentityResources.Profile(); + yield return new IdentityResources.Email(); yield return new IdentityResource(Constants.RoleScope, new[] { diff --git a/src/Squidex/Config/Identity/LazyClientStore.cs b/src/Squidex/Config/Identity/LazyClientStore.cs index 42575c22e..82bae96d7 100644 --- a/src/Squidex/Config/Identity/LazyClientStore.cs +++ b/src/Squidex/Config/Identity/LazyClientStore.cs @@ -115,6 +115,7 @@ namespace Squidex.Config.Identity { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, + IdentityServerConstants.StandardScopes.Email, Constants.ApiScope, Constants.ProfileScope, Constants.RoleScope diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 36e10ca6d..88c6af993 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -123,7 +123,7 @@ namespace Squidex.Controllers.ContentApi if (hasScript && !isFrontendClient) { - data = scriptEngine.ExecuteAndTransform(new ScriptContext { Data = data, ContentId = item.Id, User = scriptUser }, scriptText, "transform item"); + data = scriptEngine.Transform(new ScriptContext { Data = data, ContentId = item.Id, User = scriptUser }, scriptText); } itemModel.Data = data; @@ -173,7 +173,7 @@ namespace Squidex.Controllers.ContentApi if (hasScript) { - data = scriptEngine.ExecuteAndTransform(new ScriptContext { Data = data, ContentId = entity.Id, User = scriptUser }, scriptText, "transform item"); + data = scriptEngine.Transform(new ScriptContext { Data = data, ContentId = entity.Id, User = scriptUser }, scriptText); } } @@ -191,7 +191,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task PostContent([FromBody] NamedContentData request, [FromQuery] bool publish = false) { - var command = new CreateContent { ContentId = Guid.NewGuid(), Principal = User, Data = request.ToCleaned(), Publish = publish }; + var command = new CreateContent { ContentId = Guid.NewGuid(), User = User, Data = request.ToCleaned(), Publish = publish }; var context = await CommandBus.PublishAsync(command); @@ -207,7 +207,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task PutContent(Guid id, [FromBody] NamedContentData request) { - var command = new UpdateContent { ContentId = id, Principal = User, Data = request.ToCleaned() }; + var command = new UpdateContent { ContentId = id, User = User, Data = request.ToCleaned() }; var context = await CommandBus.PublishAsync(command); @@ -223,7 +223,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task PatchContent(Guid id, [FromBody] NamedContentData request) { - var command = new PatchContent { ContentId = id, Principal = User, Data = request.ToCleaned() }; + var command = new PatchContent { ContentId = id, User = User, Data = request.ToCleaned() }; var context = await CommandBus.PublishAsync(command); @@ -239,7 +239,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task PublishContent(Guid id) { - var command = new PublishContent { ContentId = id, Principal = User }; + var command = new PublishContent { ContentId = id, User = User }; await CommandBus.PublishAsync(command); @@ -252,7 +252,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task UnpublishContent(Guid id) { - var command = new UnpublishContent { ContentId = id, Principal = User }; + var command = new UnpublishContent { ContentId = id, User = User }; await CommandBus.PublishAsync(command); @@ -265,7 +265,7 @@ namespace Squidex.Controllers.ContentApi [ApiCosts(1)] public async Task PutContent(Guid id) { - var command = new DeleteContent { ContentId = id, Principal = User }; + var command = new DeleteContent { ContentId = id, User = User }; await CommandBus.PublishAsync(command); diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index ff244cbac..6dc4b5e49 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -135,6 +135,7 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone this.emitContentUpdated(this.content); this.notifyInfo('Content saved successfully.'); this.enableContentForm(); + this.populateContentForm(); }, error => { this.notifyError(error); this.enableContentForm(); diff --git a/src/Squidex/app/shared/services/auth.service.ts b/src/Squidex/app/shared/services/auth.service.ts index 6e5c97d86..8970884ce 100644 --- a/src/Squidex/app/shared/services/auth.service.ts +++ b/src/Squidex/app/shared/services/auth.service.ts @@ -75,7 +75,7 @@ export class AuthService { this.userManager = new UserManager({ client_id: 'squidex-frontend', - scope: 'squidex-api openid profile squidex-profile role', + scope: 'squidex-api openid profile email squidex-profile role', response_type: 'id_token token', redirect_uri: apiUrl.buildUrl('login;'), post_logout_redirect_uri: apiUrl.buildUrl('logout'), diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/ScriptUserTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Scripting/ScriptUserTests.cs new file mode 100644 index 000000000..489d610ca --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Scripting/ScriptUserTests.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// ScriptUserTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Security.Claims; +using FluentAssertions; +using Squidex.Infrastructure.Security; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public class ScriptUserTests + { + [Fact] + public void Should_create_script_user_from_user_principal() + { + var identity = new ClaimsIdentity(); + + identity.AddClaim(new Claim(OpenIdClaims.Subject, "1")); + identity.AddClaim(new Claim(OpenIdClaims.Email, "hello@squidex.io")); + identity.AddClaim(new Claim("claim1", "1a")); + identity.AddClaim(new Claim("claim1", "1b")); + identity.AddClaim(new Claim("claim2", "2a")); + identity.AddClaim(new Claim("claim2", "2b")); + + var principal = new ClaimsPrincipal(new[] { identity }); + + var scriptUser = ScriptUser.Create(principal); + + scriptUser.ShouldBeEquivalentTo( + new ScriptUser + { + Email = "hello@squidex.io", + Id = "1", + IsClient = false, + Claims = new Dictionary + { + { "sub", new [] { "1" } }, + { "claim1", new[] { "1a", "1b" } }, + { "claim2", new[] { "2a", "2b" } }, + { "email", new [] { "hello@squidex.io" } } + } + }); + } + + [Fact] + public void Should_create_script_user_from_client_principal() + { + var identity = new ClaimsIdentity(); + + identity.AddClaim(new Claim(OpenIdClaims.ClientId, "1")); + identity.AddClaim(new Claim("claim1", "1a")); + identity.AddClaim(new Claim("claim1", "1b")); + identity.AddClaim(new Claim("claim2", "2a")); + identity.AddClaim(new Claim("claim2", "2b")); + + var principal = new ClaimsPrincipal(new[] { identity }); + + var scriptUser = ScriptUser.Create(principal); + + scriptUser.ShouldBeEquivalentTo( + new ScriptUser + { + Id = "1", + IsClient = true, + Claims = new Dictionary + { + { "client_id", new [] { "1" } } , + { "claim1", new[] { "1a", "1b" } }, + { "claim2", new[] { "2a", "2b" } } + } + }); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandHandlerTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandHandlerTests.cs index e839e2515..a7eb7f8af 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandHandlerTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandHandlerTests.cs @@ -7,6 +7,7 @@ // ========================================================================== using System; +using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core; @@ -38,6 +39,7 @@ namespace Squidex.Domain.Apps.Write.Contents private readonly IScriptEngine scriptEngine = A.Fake(); private readonly IAppProvider appProvider = A.Fake(); private readonly IAppEntity appEntity = A.Fake(); + private readonly ClaimsPrincipal user = new ClaimsPrincipal(); private readonly NamedContentData invalidData = new NamedContentData().AddField("my-field", new ContentFieldData().SetValue(null)); private readonly NamedContentData data = new NamedContentData().AddField("my-field", new ContentFieldData().SetValue(1)); private readonly LanguagesConfig languagesConfig = LanguagesConfig.Create(Language.DE); @@ -67,7 +69,7 @@ namespace Squidex.Domain.Apps.Write.Contents { A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored, A.Ignored)).Returns(invalidData); - var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidData }); + var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidData, User = user }); await TestCreate(content, async _ => { @@ -81,7 +83,7 @@ namespace Squidex.Domain.Apps.Write.Contents A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored, A.Ignored)).Returns(data); A.CallTo(() => schemaEntity.ScriptCreate).Returns(""); - var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data }); + var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = data, User = user }); await TestCreate(content, async _ => { @@ -100,7 +102,7 @@ namespace Squidex.Domain.Apps.Write.Contents CreateContent(); - var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = invalidData }); + var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = invalidData, User = user }); await TestUpdate(content, async _ => { @@ -116,7 +118,7 @@ namespace Squidex.Domain.Apps.Write.Contents CreateContent(); - var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = data }); + var context = CreateContextForCommand(new UpdateContent { ContentId = contentId, Data = data, User = user }); await TestUpdate(content, async _ => { @@ -135,7 +137,7 @@ namespace Squidex.Domain.Apps.Write.Contents CreateContent(); - var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = invalidData }); + var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = invalidData, User = user }); await TestUpdate(content, async _ => { @@ -149,13 +151,13 @@ namespace Squidex.Domain.Apps.Write.Contents A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored, A.Ignored)).Returns(data); A.CallTo(() => schemaEntity.ScriptUpdate).Returns(""); - var path = new NamedContentData().AddField("my-field", new ContentFieldData().SetValue(3)); + var patch = new NamedContentData().AddField("my-field", new ContentFieldData().SetValue(3)); - A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored, A.Ignored)).Returns(path); + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored, A.Ignored)).Returns(patch); CreateContent(); - var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = path }); + var context = CreateContextForCommand(new PatchContent { ContentId = contentId, Data = patch, User = user }); await TestUpdate(content, async _ => { @@ -174,7 +176,7 @@ namespace Squidex.Domain.Apps.Write.Contents CreateContent(); - var context = CreateContextForCommand(new PublishContent { ContentId = contentId }); + var context = CreateContextForCommand(new PublishContent { ContentId = contentId, User = user }); await TestUpdate(content, async _ => { @@ -191,7 +193,7 @@ namespace Squidex.Domain.Apps.Write.Contents CreateContent(); - var context = CreateContextForCommand(new UnpublishContent { ContentId = contentId }); + var context = CreateContextForCommand(new UnpublishContent { ContentId = contentId, User = user }); await TestUpdate(content, async _ => { @@ -208,7 +210,7 @@ namespace Squidex.Domain.Apps.Write.Contents CreateContent(); - var command = CreateContextForCommand(new DeleteContent { ContentId = contentId }); + var command = CreateContextForCommand(new DeleteContent { ContentId = contentId, User = user }); await TestUpdate(content, async _ => {