From b92daa0c06b58ded90922d2e45957e126861ea4e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 27 Nov 2016 21:31:04 +0100 Subject: [PATCH] Namespaces fixed and user token support --- .../CQRS/Commands/IUserCommand.cs | 2 +- .../Json/LanguageConverter.cs | 2 +- .../Json/UserTokenConverter.cs | 31 +++++ src/Squidex.Infrastructure/UserToken.cs | 61 +++++++- src/Squidex.Write/Apps/AppDomainObject.cs | 2 +- src/Squidex.Write/Apps/Commands/CreateApp.cs | 2 +- src/Squidex/Config/Domain/Serializers.cs | 1 + .../CommandHandlers/EnrichWithUserHandler.cs | 35 +++-- .../LanguageTests.cs | 1 + .../UserTokenTests.cs | 131 +++++++++++++++++- .../Apps/AppCommandHandlerTests.cs | 8 +- .../Apps/AppDomainObjectTests.cs | 14 +- 12 files changed, 252 insertions(+), 38 deletions(-) create mode 100644 src/Squidex.Infrastructure/Json/UserTokenConverter.cs diff --git a/src/Squidex.Infrastructure/CQRS/Commands/IUserCommand.cs b/src/Squidex.Infrastructure/CQRS/Commands/IUserCommand.cs index ee01a9905..ee6d10154 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/IUserCommand.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/IUserCommand.cs @@ -10,6 +10,6 @@ namespace Squidex.Infrastructure.CQRS.Commands { public interface IUserCommand : ICommand { - string UserId { get; set; } + UserToken User { get; set; } } } diff --git a/src/Squidex.Infrastructure/Json/LanguageConverter.cs b/src/Squidex.Infrastructure/Json/LanguageConverter.cs index 7e022eb79..dad69ef26 100644 --- a/src/Squidex.Infrastructure/Json/LanguageConverter.cs +++ b/src/Squidex.Infrastructure/Json/LanguageConverter.cs @@ -20,7 +20,7 @@ namespace Squidex.Infrastructure.Json public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - return reader.TokenType == JsonToken.Null ? null : Language.GetLanguage((string) reader.Value); + return reader.TokenType == JsonToken.Null ? null : Language.GetLanguage((string)reader.Value); } public override bool CanConvert(Type objectType) diff --git a/src/Squidex.Infrastructure/Json/UserTokenConverter.cs b/src/Squidex.Infrastructure/Json/UserTokenConverter.cs new file mode 100644 index 000000000..1b04b2d9f --- /dev/null +++ b/src/Squidex.Infrastructure/Json/UserTokenConverter.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// UserTokenConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; + +namespace Squidex.Infrastructure.Json +{ + public sealed class UserTokenConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return reader.TokenType == JsonToken.Null ? null : UserToken.Parse((string)reader.Value); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(UserToken); + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/UserToken.cs b/src/Squidex.Infrastructure/UserToken.cs index 57cc66c49..3d8ef974a 100644 --- a/src/Squidex.Infrastructure/UserToken.cs +++ b/src/Squidex.Infrastructure/UserToken.cs @@ -1,11 +1,64 @@ -using System; -using System.Collections.Generic; +// ========================================================================== +// UserToken.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; using System.Linq; -using System.Threading.Tasks; namespace Squidex.Infrastructure { - public sealed class UserToken + public sealed class UserToken : IEquatable { + public string Type { get; } + + public string Identifier { get; } + + public UserToken(string type, string identifier) + { + Guard.NotNullOrEmpty(type, nameof(type)); + Guard.NotNullOrEmpty(identifier, nameof(identifier)); + + Type = type.ToLowerInvariant(); + + Identifier = identifier; + } + + public static UserToken Parse(string input) + { + Guard.NotNullOrEmpty(input, nameof(input)); + + var parts = input.Split(new [] { ':' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length < 2) + { + throw new ArgumentException("Input must have more than 2 parts divided by colon", nameof(input)); + } + + return new UserToken(parts[0], string.Join(":", parts.Skip(1))); + } + + public override string ToString() + { + return $"{Type}:{Identifier}"; + } + + public override bool Equals(object obj) + { + return Equals(obj as UserToken); + } + + public bool Equals(UserToken other) + { + return other != null && (ReferenceEquals(this, other) || (Type.Equals(other.Type) && Identifier.Equals(other.Identifier))); + } + + public override int GetHashCode() + { + return (Type.GetHashCode() * 397) ^ (Identifier.GetHashCode()); + } } } diff --git a/src/Squidex.Write/Apps/AppDomainObject.cs b/src/Squidex.Write/Apps/AppDomainObject.cs index b0564275c..92efa5255 100644 --- a/src/Squidex.Write/Apps/AppDomainObject.cs +++ b/src/Squidex.Write/Apps/AppDomainObject.cs @@ -175,7 +175,7 @@ namespace Squidex.Write.Apps private static AppContributorAssigned CreateInitialOwner(IUserCommand command) { - return new AppContributorAssigned { ContributorId = command.UserId, Permission = PermissionLevel.Owner }; + return new AppContributorAssigned { ContributorId = command.User.Identifier, Permission = PermissionLevel.Owner }; } private void ThrowIfNotCreated() diff --git a/src/Squidex.Write/Apps/Commands/CreateApp.cs b/src/Squidex.Write/Apps/Commands/CreateApp.cs index 9db9abb27..ee9294669 100644 --- a/src/Squidex.Write/Apps/Commands/CreateApp.cs +++ b/src/Squidex.Write/Apps/Commands/CreateApp.cs @@ -17,7 +17,7 @@ namespace Squidex.Write.Apps.Commands { public string Name { get; set; } - public string UserId { get; set; } + public UserToken User { get; set; } public CreateApp() { diff --git a/src/Squidex/Config/Domain/Serializers.cs b/src/Squidex/Config/Domain/Serializers.cs index 8e1c15ce8..9c2422a1b 100644 --- a/src/Squidex/Config/Domain/Serializers.cs +++ b/src/Squidex/Config/Domain/Serializers.cs @@ -26,6 +26,7 @@ namespace Squidex.Config.Domain settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); settings.Converters.Add(new LanguageConverter()); settings.Converters.Add(new PropertiesBagConverter()); + settings.Converters.Add(new UserTokenConverter()); settings.NullValueHandling = NullValueHandling.Ignore; settings.DateFormatHandling = DateFormatHandling.IsoDateFormat; settings.DateParseHandling = DateParseHandling.DateTime; diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithUserHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithUserHandler.cs index 601e40403..3c7656c7e 100644 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithUserHandler.cs +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithUserHandler.cs @@ -9,6 +9,7 @@ using System.Security; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Security; // ReSharper disable InvertIf @@ -30,27 +31,33 @@ namespace Squidex.Pipeline.CommandHandlers if (subjectCommand != null) { - var subjectId = httpContextAccessor.HttpContext.User.OpenIdSubject(); + var userToken = + FindUserFromSubject() ?? + FindUserFromClient(); - if (subjectId == null) + if (userToken == null) { - var clientId = httpContextAccessor.HttpContext.User.OpenIdClientId(); - - if (clientId != null) - { - subjectId = $"Client:" - } - } - - if (subjectId == null) - { - throw new SecurityException("No user with subject id available"); + throw new SecurityException("No user with subject or client id available"); } - subjectCommand.UserId = subjectId; + subjectCommand.User = userToken; } return Task.FromResult(false); } + + private UserToken FindUserFromSubject() + { + var subjectId = httpContextAccessor.HttpContext.User.OpenIdSubject(); + + return subjectId == null ? null : new UserToken("subject", subjectId); + } + + private UserToken FindUserFromClient() + { + var clientId = httpContextAccessor.HttpContext.User.OpenIdClientId(); + + return clientId == null ? null : new UserToken("client", clientId); + } } } diff --git a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs index b93a61cb5..2018a209a 100644 --- a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs +++ b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs @@ -21,6 +21,7 @@ namespace Squidex.Infrastructure static LanguageTests() { serializerSettings.Converters.Add(new LanguageConverter()); + serializerSettings.NullValueHandling = NullValueHandling.Include; } [Theory] diff --git a/tests/Squidex.Infrastructure.Tests/UserTokenTests.cs b/tests/Squidex.Infrastructure.Tests/UserTokenTests.cs index 5e2cd9793..1effb0188 100644 --- a/tests/Squidex.Infrastructure.Tests/UserTokenTests.cs +++ b/tests/Squidex.Infrastructure.Tests/UserTokenTests.cs @@ -1,17 +1,138 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +// ========================================================================== +// UserTokenTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Squidex.Infrastructure.Json; using Xunit; +// ReSharper disable RedundantCast namespace Squidex.Infrastructure { public class UserTokenTests { + private static readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings(); + + static UserTokenTests() + { + serializerSettings.Converters.Add(new UserTokenConverter()); + serializerSettings.NullValueHandling = NullValueHandling.Include; + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(":")] + [InlineData("user")] + public void Should_throw_if_parsing_invalid_input(string input) + { + Assert.Throws(() => UserToken.Parse(input)); + } + + [Fact] + public void Should_instantiate_token() + { + var token = new UserToken("client", "client1"); + + Assert.Equal("client", token.Type); + Assert.Equal("client1", token.Identifier); + } + + [Fact] + public void Should_instantiate_token_and_lower_type() + { + var token = new UserToken("Client", "client1"); + + Assert.Equal("client", token.Type); + Assert.Equal("client1", token.Identifier); + } + [Fact] public void Should_parse_user_token_from_string() { - var token = UserToken.Parse("") + var token = UserToken.Parse("client:client1"); + + Assert.Equal("client", token.Type); + Assert.Equal("client1", token.Identifier); + } + + [Fact] + public void Should_parse_user_token_with_colon_in_identifier() + { + var token = UserToken.Parse("client:client1:app"); + + Assert.Equal("client", token.Type); + Assert.Equal("client1:app", token.Identifier); + } + + [Fact] + public void Should_convert_user_token_to_string() + { + var token = UserToken.Parse("client:client1"); + + Assert.Equal("client:client1", token.ToString()); + } + + [Fact] + public void Should_make_correct_equal_comparisons() + { + var token1a = UserToken.Parse("client:client1"); + var token1b = UserToken.Parse("client:client1"); + var token2 = UserToken.Parse("client:client2"); + + Assert.True(token1a.Equals(token1b)); + + Assert.False(token1a.Equals(token2)); + } + + [Fact] + public void Should_make_correct_object_equal_comparisons() + { + var token1a = UserToken.Parse("client:client1"); + + object token1b = UserToken.Parse("client:client1"); + object token2 = UserToken.Parse("client:client2"); + + Assert.True(token1a.Equals(token1b)); + + Assert.False(token1a.Equals(token2)); + } + + [Fact] + public void Should_provide_correct_hash_codes() + { + var token1a = UserToken.Parse("client:client1"); + var token1b = UserToken.Parse("client:client1"); + var token2 = UserToken.Parse("client:client2"); + + Assert.Equal(token1a.GetHashCode(), token1b.GetHashCode()); + + Assert.NotEqual(token1a.GetHashCode(), token2.GetHashCode()); + } + + [Fact] + public void Should_serialize_and_deserialize_null_token() + { + var input = Tuple.Create(null); + var json = JsonConvert.SerializeObject(input, serializerSettings); + var output = JsonConvert.DeserializeObject>(json, serializerSettings); + + Assert.Equal(output.Item1, input.Item1); + } + + [Fact] + public void Should_serialize_and_deserialize_valid_token() + { + var input = Tuple.Create(UserToken.Parse("client:client1")); + var json = JsonConvert.SerializeObject(input, serializerSettings); + var output = JsonConvert.DeserializeObject>(json, serializerSettings); + + Assert.Equal(output.Item1, input.Item1); } } } diff --git a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs index 14d37a559..ca264d4b2 100644 --- a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs @@ -33,7 +33,7 @@ namespace Squidex.Write.Apps private readonly Mock userRepository = new Mock(); private readonly AppCommandHandler sut; private readonly AppDomainObject app; - private readonly string subjectId = Guid.NewGuid().ToString(); + private readonly UserToken subjectId = new UserToken("subject", Guid.NewGuid().ToString()); private readonly string contributorId = Guid.NewGuid().ToString(); private readonly string clientSecret = Guid.NewGuid().ToString(); private readonly string clientName = "client"; @@ -54,7 +54,7 @@ namespace Squidex.Write.Apps [Fact] public async Task Create_should_throw_if_a_name_with_same_name_already_exists() { - var command = new CreateApp { Name = appName, AggregateId = Id, UserId = subjectId }; + var command = new CreateApp { Name = appName, AggregateId = Id, User = subjectId }; var context = new CommandContext(command); appRepository.Setup(x => x.FindAppByNameAsync(appName)).Returns(Task.FromResult(new Mock().Object)).Verifiable(); @@ -70,7 +70,7 @@ namespace Squidex.Write.Apps [Fact] public async Task Create_should_create_app_if_name_is_free() { - var command = new CreateApp { Name = appName, AggregateId = Id, UserId = subjectId }; + var command = new CreateApp { Name = appName, AggregateId = Id, User = subjectId }; var context = new CommandContext(command); appRepository.Setup(x => x.FindAppByNameAsync(appName)).Returns(Task.FromResult(null)).Verifiable(); @@ -200,7 +200,7 @@ namespace Squidex.Write.Apps private AppDomainObject CreateApp() { - app.Create(new CreateApp { Name = appName, UserId = subjectId }); + app.Create(new CreateApp { Name = appName, User = subjectId }); return app; } diff --git a/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs index 6f60c6817..65b93271a 100644 --- a/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs @@ -26,7 +26,7 @@ namespace Squidex.Write.Apps { private const string TestName = "app"; private readonly AppDomainObject sut; - private readonly string subjectId = Guid.NewGuid().ToString(); + private readonly UserToken user = new UserToken("subject", Guid.NewGuid().ToString()); private readonly string contributorId = Guid.NewGuid().ToString(); private readonly string clientSecret = Guid.NewGuid().ToString(); private readonly string clientName = "client"; @@ -54,17 +54,17 @@ namespace Squidex.Write.Apps [Fact] public void Create_should_specify_name_and_owner() { - sut.Create(new CreateApp { Name = TestName, UserId = subjectId }); + sut.Create(new CreateApp { Name = TestName, User = user }); Assert.Equal(TestName, sut.Name); - Assert.Equal(PermissionLevel.Owner, sut.Contributors[subjectId]); + Assert.Equal(PermissionLevel.Owner, sut.Contributors[user.Identifier]); sut.GetUncomittedEvents().Select(x => x.Payload).ToArray() .ShouldBeEquivalentTo( new IEvent[] { new AppCreated { Name = TestName }, - new AppContributorAssigned { ContributorId = subjectId, Permission = PermissionLevel.Owner }, + new AppContributorAssigned { ContributorId = user.Identifier, Permission = PermissionLevel.Owner }, new AppLanguagesConfigured { Languages= new List { Language.GetLanguage("en") } } }); } @@ -86,7 +86,7 @@ namespace Squidex.Write.Apps { CreateApp(); - Assert.Throws(() => sut.AssignContributor(new AssignContributor { ContributorId = subjectId, Permission = PermissionLevel.Editor })); + Assert.Throws(() => sut.AssignContributor(new AssignContributor { ContributorId = user.Identifier, Permission = PermissionLevel.Editor })); } [Fact] @@ -123,7 +123,7 @@ namespace Squidex.Write.Apps { CreateApp(); - Assert.Throws(() => sut.RemoveContributor(new RemoveContributor { ContributorId = subjectId })); + Assert.Throws(() => sut.RemoveContributor(new RemoveContributor { ContributorId = user.Identifier })); } [Fact] @@ -271,7 +271,7 @@ namespace Squidex.Write.Apps private void CreateApp() { - sut.Create(new CreateApp { Name = TestName, UserId = subjectId }); + sut.Create(new CreateApp { Name = TestName, User = user }); ((IAggregate)sut).ClearUncommittedEvents(); }