diff --git a/global.json b/global.json new file mode 100644 index 000000000..110af6027 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "2.0.0" + } +} diff --git a/src/Squidex.Domain.Apps.Core/Scripting/IScriptEngine.cs b/src/Squidex.Domain.Apps.Core/Scripting/IScriptEngine.cs index f137d66e9..fff7b509c 100644 --- a/src/Squidex.Domain.Apps.Core/Scripting/IScriptEngine.cs +++ b/src/Squidex.Domain.Apps.Core/Scripting/IScriptEngine.cs @@ -6,15 +6,14 @@ // All rights reserved. // ========================================================================== -using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; namespace Squidex.Domain.Apps.Core.Scripting { public interface IScriptEngine { - Task ExecuteAsync(ScriptContext context, string operationName, string script); + void Execute(ScriptContext context, string script, string operationName); - Task ExecuteAndTransformAsync(ScriptContext context, string operationName, string script); + NamedContentData ExecuteAndTransform(ScriptContext context, string script, string operationName, bool failOnError = false); } } diff --git a/src/Squidex.Domain.Apps.Core/Scripting/JurassicScriptEngine.cs b/src/Squidex.Domain.Apps.Core/Scripting/JurassicScriptEngine.cs index 105614039..6d1849930 100644 --- a/src/Squidex.Domain.Apps.Core/Scripting/JurassicScriptEngine.cs +++ b/src/Squidex.Domain.Apps.Core/Scripting/JurassicScriptEngine.cs @@ -7,15 +7,13 @@ // ========================================================================== using System; -using System.Security; -using System.Threading.Tasks; using Jurassic; using Jurassic.Library; using Newtonsoft.Json; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure; -using Squidex.Infrastructure.Tasks; +// ReSharper disable InvertIf // ReSharper disable ConvertToLambdaExpression namespace Squidex.Domain.Apps.Core.Scripting @@ -31,66 +29,75 @@ namespace Squidex.Domain.Apps.Core.Scripting this.serializerSettings = serializerSettings; } - public Task ExecuteAsync(ScriptContext context, string operationName, string script) + public void Execute(ScriptContext context, string script, string operationName) { Guard.NotNull(context, nameof(context)); if (!string.IsNullOrWhiteSpace(script)) { - return TaskHelper.False; - } - - var engine = CreateScriptEngine(context, operationName); - - engine.Execute(script); + var engine = CreateScriptEngine(context, operationName); - return TaskHelper.False; + Execute(script, operationName, engine, true); + } } - public Task ExecuteAndTransformAsync(ScriptContext context, string operationName, string script) + public NamedContentData ExecuteAndTransform(ScriptContext context, string script, string operationName, bool failOnError = false) { Guard.NotNull(context, nameof(context)); + var result = context.Data; + if (!string.IsNullOrWhiteSpace(script)) { - return Task.FromResult(context.Data); - } + var engine = CreateScriptEngine(context, operationName); - var result = context.Data; + engine.SetGlobalFunction("replace", new Action(data => + { + try + { + result = JsonConvert.DeserializeObject(JSONObject.Stringify(engine, data)); + } + catch + { + result = new NamedContentData(); + } + })); + + Execute(script, operationName, engine, failOnError); + } - var engine = CreateScriptEngine(context, operationName); + return result; + } - engine.SetGlobalFunction("replace", new Action(data => + private static void Execute(string script, string operationName, ScriptEngine engine, bool failOnError = false) + { + try { - try - { - result = JsonConvert.DeserializeObject(JSONObject.Stringify(engine, data)); - } - catch + engine.Execute(script); + } + catch (JavaScriptException ex) + { + if (failOnError) { - result = new NamedContentData(); + throw new ValidationException($"Failed to {operationName} with javascript error.", new ValidationError(ex.Message)); } - })); - - engine.Execute(script); - - return Task.FromResult(result); + } } private ScriptEngine CreateScriptEngine(ScriptContext context, string operationName) { Guard.NotNullOrEmpty(operationName, nameof(operationName)); - var engine = new ScriptEngine(); + var engine = new ScriptEngine { ForceStrictMode = true }; engine.SetGlobalFunction("disallow", new Action(message => { - throw new SecurityException(message); + throw new DomainForbiddenException(!string.IsNullOrWhiteSpace(message) ? message : "Not allowed"); })); engine.SetGlobalFunction("reject", new Action(message => { - throw new ValidationException($"Failed to '{operationName}", !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null); + throw new ValidationException($"Failed to {operationName}", !string.IsNullOrWhiteSpace(message) ? new[] { new ValidationError(message) } : null); })); var json = JsonConvert.SerializeObject(context, serializerSettings); diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj b/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj index 62570d28e..8f024eb56 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj +++ b/src/Squidex.Domain.Apps.Read.MongoDb/Squidex.Domain.Apps.Read.MongoDb.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Squidex.Domain.Users.MongoDb/MongoRole.cs b/src/Squidex.Domain.Users.MongoDb/MongoRole.cs new file mode 100644 index 000000000..682ef8b9c --- /dev/null +++ b/src/Squidex.Domain.Users.MongoDb/MongoRole.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// MongoRole.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Squidex.Domain.Users.MongoDb +{ + public sealed class MongoRole : IRole + { + [BsonRepresentation(BsonType.ObjectId)] + [BsonElement] + public string Id { get; set; } + + [BsonRequired] + [BsonElement] + public string Name { get; set; } + + [BsonRequired] + [BsonElement] + public string NormalizedName { get; set; } + } +} diff --git a/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs b/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs index 2ff23c1fc..c7340d010 100644 --- a/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs +++ b/src/Squidex.Domain.Users.MongoDb/MongoRoleStore.cs @@ -9,82 +9,101 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.MongoDB; using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Users.MongoDb { - public sealed class MongoRoleStore : IRoleStore, IRoleFactory + public sealed class MongoRoleStore : MongoRepositoryBase, IRoleStore, IRoleFactory { - private readonly RoleStore innerStore; - public MongoRoleStore(IMongoDatabase database) + : base(database) { - var rolesCollection = database.GetCollection("Identity_Roles"); + } - IndexChecks.EnsureUniqueIndexOnNormalizedRoleName(rolesCollection); + protected override string CollectionName() + { + return "Identity_Roles"; + } - innerStore = new RoleStore(rolesCollection); + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NormalizedName), new CreateIndexOptions { Unique = true }); + } + + protected override MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; } public void Dispose() { - innerStore.Dispose(); } public IRole Create(string name) { - return new WrappedIdentityRole { Name = name }; + return new MongoRole { Name = name }; } public async Task FindByIdAsync(string roleId, CancellationToken cancellationToken) { - return await innerStore.FindByIdAsync(roleId, cancellationToken); + return await Collection.Find(x => x.Id == roleId).FirstOrDefaultAsync(cancellationToken); } public async Task FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken) { - return await innerStore.FindByNameAsync(normalizedRoleName, cancellationToken); + return await Collection.Find(x => x.NormalizedName == normalizedRoleName).FirstOrDefaultAsync(cancellationToken); } - public Task CreateAsync(IRole role, CancellationToken cancellationToken) + public async Task CreateAsync(IRole role, CancellationToken cancellationToken) { - return innerStore.CreateAsync((WrappedIdentityRole)role, cancellationToken); + await Collection.InsertOneAsync((MongoRole)role, null, cancellationToken); + + return IdentityResult.Success; } - public Task UpdateAsync(IRole role, CancellationToken cancellationToken) + public async Task UpdateAsync(IRole role, CancellationToken cancellationToken) { - return innerStore.UpdateAsync((WrappedIdentityRole)role, cancellationToken); + await Collection.ReplaceOneAsync(x => x.Id == ((MongoRole)role).Id, (MongoRole)role, null, cancellationToken); + + return IdentityResult.Success; } - public Task DeleteAsync(IRole role, CancellationToken cancellationToken) + public async Task DeleteAsync(IRole role, CancellationToken cancellationToken) { - return innerStore.DeleteAsync((WrappedIdentityRole)role, cancellationToken); + await Collection.DeleteOneAsync(x => x.Id == ((MongoRole)role).Id, null, cancellationToken); + + return IdentityResult.Success; } public Task GetRoleIdAsync(IRole role, CancellationToken cancellationToken) { - return innerStore.GetRoleIdAsync((WrappedIdentityRole)role, cancellationToken); + return Task.FromResult(((MongoRole)role).Id); } public Task GetRoleNameAsync(IRole role, CancellationToken cancellationToken) { - return innerStore.GetRoleNameAsync((WrappedIdentityRole)role, cancellationToken); + return Task.FromResult(((MongoRole)role).Name); } - public Task SetRoleNameAsync(IRole role, string roleName, CancellationToken cancellationToken) + public Task GetNormalizedRoleNameAsync(IRole role, CancellationToken cancellationToken) { - return innerStore.SetRoleNameAsync((WrappedIdentityRole)role, roleName, cancellationToken); + return Task.FromResult(((MongoRole)role).NormalizedName); } - public Task GetNormalizedRoleNameAsync(IRole role, CancellationToken cancellationToken) + public Task SetRoleNameAsync(IRole role, string roleName, CancellationToken cancellationToken) { - return innerStore.GetNormalizedRoleNameAsync((WrappedIdentityRole)role, cancellationToken); + ((MongoRole)role).Name = roleName; + + return TaskHelper.Done; } public Task SetNormalizedRoleNameAsync(IRole role, string normalizedName, CancellationToken cancellationToken) { - return innerStore.SetNormalizedRoleNameAsync((WrappedIdentityRole)role, normalizedName, cancellationToken); + ((MongoRole)role).NormalizedName = normalizedName; + + return TaskHelper.Done; } } } diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUser.cs b/src/Squidex.Domain.Users.MongoDb/MongoUser.cs new file mode 100644 index 000000000..191a66016 --- /dev/null +++ b/src/Squidex.Domain.Users.MongoDb/MongoUser.cs @@ -0,0 +1,199 @@ +// ========================================================================== +// MongoUser.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Infrastructure; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Users.MongoDb +{ + public sealed class MongoUser : IUser + { + [BsonRepresentation(BsonType.ObjectId)] + [BsonElement] + public string Id { get; set; } + + [BsonIgnoreIfNull] + [BsonElement] + public string SecurityStamp { get; set; } + + [BsonRequired] + [BsonElement] + public string UserName { get; set; } + + [BsonRequired] + [BsonElement] + public string NormalizedUserName { get; set; } + + [BsonRequired] + [BsonElement] + public string Email { get; set; } + + [BsonRequired] + [BsonElement] + public string NormalizedEmail { get; set; } + + [BsonIgnoreIfNull] + [BsonElement] + public string PhoneNumber { get; set; } + + [BsonIgnoreIfNull] + [BsonElement] + public string PasswordHash { get; set; } + + [BsonRequired] + [BsonElement] + public bool EmailConfirmed { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement] + public bool PhoneNumberConfirmed { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement] + public bool TwoFactorEnabled { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement] + public bool LockoutEnabled { get; set; } + + [BsonIgnoreIfNull] + [BsonElement] + public DateTime? LockoutEndDateUtc { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement] + public int AccessFailedCount { get; set; } + + [BsonRequired] + [BsonElement] + public List Roles { get; set; } = new List(); + + [BsonRequired] + [BsonElement] + public List Claims { get; set; } = new List(); + + [BsonRequired] + [BsonElement] + public List Tokens { get; set; } = new List(); + + [BsonRequired] + [BsonElement] + public List Logins { get; set; } = new List(); + + public bool IsLocked + { + get { return LockoutEndDateUtc != null && LockoutEndDateUtc.Value > DateTime.UtcNow; } + } + + IReadOnlyList IUser.Claims + { + get { return Claims.Select(x => new Claim(x.Type, x.Value)).ToList(); } + } + + IReadOnlyList IUser.Logins + { + get { return Logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList(); } + } + + public MongoUser() + { + Id = ObjectId.GenerateNewId().ToString(); + } + + public void UpdateEmail(string email) + { + Email = UserName = email; + } + + public void AddRole(string role) + { + Roles.Add(role); + } + + public void RemoveRole(string role) + { + Roles.Remove(role); + } + + public void AddLogin(UserLoginInfo login) + { + Logins.Add(login); + } + + public void RemoveLogin(string loginProvider, string providerKey) + { + Logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey); + } + + public void AddClaim(Claim claim) + { + Claims.Add(claim); + } + + public void AddClaims(IEnumerable claims) + { + claims.Foreach(AddClaim); + } + + public void RemoveClaim(Claim claim) + { + Claims.RemoveAll(c => c.Type == claim.Type && c.Value == claim.Value); + } + + public void RemoveClaims(IEnumerable claims) + { + claims.Foreach(RemoveClaim); + } + + public void SetClaim(string type, string value) + { + SetClaim(new Claim(type, value)); + } + + public void SetClaim(Claim claim) + { + ReplaceClaim(claim, claim); + } + + public string GetToken(string loginProider, string name) + { + return Tokens.FirstOrDefault(t => t.LoginProvider == loginProider && t.Name == name)?.Value; + } + + public void AddToken(string loginProvider, string name, string value) + { + Tokens.Add(new MongoUserToken { LoginProvider = loginProvider, Name = name, Value = value }); + } + + public void RemoveToken(string loginProvider, string name) + { + Tokens.RemoveAll(t => t.LoginProvider == loginProvider && t.Name == name); + } + + public void ReplaceClaim(Claim existingClaim, Claim newClaim) + { + RemoveClaim(existingClaim); + + AddClaim(newClaim); + } + + public void SetToken(string loginProider, string name, string value) + { + RemoveToken(loginProider, name); + + AddToken(loginProider, name, value); + } + } +} diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUserClaim.cs b/src/Squidex.Domain.Users.MongoDb/MongoUserClaim.cs new file mode 100644 index 000000000..d9632e439 --- /dev/null +++ b/src/Squidex.Domain.Users.MongoDb/MongoUserClaim.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// MongoUserClaim.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Security.Claims; +using MongoDB.Bson.Serialization.Attributes; + +namespace Squidex.Domain.Users.MongoDb +{ + public sealed class MongoUserClaim + { + [BsonRequired] + [BsonElement] + public string Type { get; set; } + + [BsonRequired] + [BsonElement] + public string Value { get; set; } + + public static implicit operator MongoUserClaim(Claim claim) + { + return new MongoUserClaim { Type = claim.Type, Value = claim.Value }; + } + + public static implicit operator Claim(MongoUserClaim userClaim) + { + return new Claim(userClaim.Type, userClaim.Value); + } + } +} diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUserLogin.cs b/src/Squidex.Domain.Users.MongoDb/MongoUserLogin.cs new file mode 100644 index 000000000..899c0daea --- /dev/null +++ b/src/Squidex.Domain.Users.MongoDb/MongoUserLogin.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// MongoUserLogin.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Microsoft.AspNetCore.Identity; +using MongoDB.Bson.Serialization.Attributes; + +namespace Squidex.Domain.Users.MongoDb +{ + public sealed class MongoUserLogin + { + [BsonRequired] + [BsonElement] + public string LoginProvider { get; set; } + + [BsonRequired] + [BsonElement] + public string ProviderDisplayName { get; set; } + + [BsonRequired] + [BsonElement] + public string ProviderKey { get; set; } + + public static implicit operator MongoUserLogin(UserLoginInfo login) + { + return new MongoUserLogin + { + LoginProvider = login.LoginProvider, + ProviderKey = login.ProviderKey, + ProviderDisplayName = login.ProviderDisplayName + }; + } + + public static implicit operator UserLoginInfo(MongoUserLogin userLogin) + { + return new UserLoginInfo(userLogin.LoginProvider, userLogin.ProviderKey, userLogin.ProviderDisplayName); + } + } +} diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs b/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs index 23ca128be..964e9a990 100644 --- a/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs +++ b/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs @@ -13,13 +13,15 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.MongoDB; using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; +using Squidex.Infrastructure.Tasks; using Squidex.Shared.Users; namespace Squidex.Domain.Users.MongoDb { public sealed class MongoUserStore : + MongoRepositoryBase, IUserPasswordStore, IUserRoleStore, IUserLoginStore, @@ -34,296 +36,357 @@ namespace Squidex.Domain.Users.MongoDb IUserResolver, IQueryableUserStore { - private readonly UserStore innerStore; - public MongoUserStore(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() { - var usersCollection = database.GetCollection("Identity_Users"); + return "Identity_Users"; + } - IndexChecks.EnsureUniqueIndexOnNormalizedEmail(usersCollection); - IndexChecks.EnsureUniqueIndexOnNormalizedUserName(usersCollection); + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return Task.WhenAll( + collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NormalizedUserName), new CreateIndexOptions { Unique = true }), + collection.Indexes.CreateOneAsync(Index.Ascending(x => x.NormalizedEmail), new CreateIndexOptions { Unique = true })); + } - innerStore = new UserStore(usersCollection); + protected override MongoCollectionSettings CollectionSettings() + { + return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; } public void Dispose() { - innerStore.Dispose(); } public IQueryable Users { - get { return innerStore.Users; } + get { return Collection.AsQueryable(); } } public IUser Create(string email) { - return new WrappedIdentityUser { Email = email, UserName = email }; + return new MongoUser { Email = email, UserName = email }; } - public async Task FindByIdAsync(string userId) + public async Task FindByIdAsync(string id) { - return await innerStore.FindByIdAsync(userId, CancellationToken.None); + return await Collection.Find(x => x.Id == id).FirstOrDefaultAsync(); } public async Task FindByIdAsync(string userId, CancellationToken cancellationToken) { - return await innerStore.FindByIdAsync(userId, cancellationToken); + return await Collection.Find(x => x.Id == userId).FirstOrDefaultAsync(cancellationToken); } public async Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) { - return await innerStore.FindByEmailAsync(normalizedEmail, cancellationToken); + return await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken); } public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) { - return await innerStore.FindByNameAsync(normalizedUserName, cancellationToken); + return await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken); } public async Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) { - return await innerStore.FindByLoginAsync(loginProvider, providerKey, cancellationToken); + return await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken); } public async Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) { - return (await innerStore.GetUsersForClaimAsync(claim, cancellationToken)).OfType().ToList(); + return (await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken)).OfType().ToList(); } public async Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) { - return (await innerStore.GetUsersInRoleAsync(roleName, cancellationToken)).OfType().ToList(); + return (await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken)).OfType().ToList(); } - public Task CreateAsync(IUser user, CancellationToken cancellationToken) + public async Task CreateAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.CreateAsync((WrappedIdentityUser)user, cancellationToken); + await Collection.InsertOneAsync((MongoUser)user, null, cancellationToken); + + return IdentityResult.Success; } - public Task UpdateAsync(IUser user, CancellationToken cancellationToken) + public async Task UpdateAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.UpdateAsync((WrappedIdentityUser)user, cancellationToken); + await Collection.ReplaceOneAsync(x => x.Id == user.Id, (MongoUser)user, null, cancellationToken); + + return IdentityResult.Success; } - public Task DeleteAsync(IUser user, CancellationToken cancellationToken) + public async Task DeleteAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.DeleteAsync((WrappedIdentityUser)user, cancellationToken); + await Collection.DeleteOneAsync(x => x.Id == user.Id, null, cancellationToken); + + return IdentityResult.Success; } public Task GetUserIdAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.GetUserIdAsync((WrappedIdentityUser)user, cancellationToken); + return Task.FromResult(((MongoUser)user).Id); } public Task GetUserNameAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.GetUserNameAsync((WrappedIdentityUser)user, cancellationToken); + return Task.FromResult(((MongoUser)user).UserName); } - public Task SetUserNameAsync(IUser user, string userName, CancellationToken cancellationToken) + public Task GetNormalizedUserNameAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.SetUserNameAsync((WrappedIdentityUser)user, userName, cancellationToken); + return Task.FromResult(((MongoUser)user).NormalizedUserName); } - public Task GetNormalizedUserNameAsync(IUser user, CancellationToken cancellationToken) + public Task GetPasswordHashAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.GetNormalizedUserNameAsync((WrappedIdentityUser)user, cancellationToken); + return Task.FromResult(((MongoUser)user).PasswordHash); } - public Task SetNormalizedUserNameAsync(IUser user, string normalizedName, CancellationToken cancellationToken) + public Task> GetRolesAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.SetNormalizedUserNameAsync((WrappedIdentityUser)user, normalizedName, cancellationToken); + return Task.FromResult>(((MongoUser)user).Roles); } - public Task GetPasswordHashAsync(IUser user, CancellationToken cancellationToken) + public Task IsInRoleAsync(IUser user, string roleName, CancellationToken cancellationToken) { - return innerStore.GetPasswordHashAsync((WrappedIdentityUser)user, cancellationToken); + return Task.FromResult(((MongoUser)user).Roles.Contains(roleName)); } - public Task SetPasswordHashAsync(IUser user, string passwordHash, CancellationToken cancellationToken) + public Task> GetLoginsAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.SetPasswordHashAsync((WrappedIdentityUser)user, passwordHash, cancellationToken); + return Task.FromResult>(((MongoUser)user).Logins.Select(x => (UserLoginInfo)x).ToList()); } - public Task AddToRoleAsync(IUser user, string roleName, CancellationToken cancellationToken) + public Task GetSecurityStampAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.AddToRoleAsync((WrappedIdentityUser)user, roleName, cancellationToken); + return Task.FromResult(((MongoUser)user).SecurityStamp); } - public Task RemoveFromRoleAsync(IUser user, string roleName, CancellationToken cancellationToken) + public Task GetEmailAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.RemoveFromRoleAsync((WrappedIdentityUser)user, roleName, cancellationToken); + return Task.FromResult(((MongoUser)user).Email); } - public Task> GetRolesAsync(IUser user, CancellationToken cancellationToken) + public Task GetEmailConfirmedAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.GetRolesAsync((WrappedIdentityUser)user, cancellationToken); + return Task.FromResult(((MongoUser)user).EmailConfirmed); } - public Task IsInRoleAsync(IUser user, string roleName, CancellationToken cancellationToken) + public Task GetNormalizedEmailAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.IsInRoleAsync((WrappedIdentityUser)user, roleName, cancellationToken); + return Task.FromResult(((MongoUser)user).NormalizedEmail); } - public Task AddLoginAsync(IUser user, UserLoginInfo login, CancellationToken cancellationToken) + public Task> GetClaimsAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.AddLoginAsync((WrappedIdentityUser)user, login, cancellationToken); + return Task.FromResult>(((MongoUser)user).Claims.Select(x => (Claim)x).ToList()); } - public Task RemoveLoginAsync(IUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) + public Task GetPhoneNumberAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.RemoveLoginAsync((WrappedIdentityUser)user, loginProvider, providerKey, cancellationToken); + return Task.FromResult(((MongoUser)user).PhoneNumber); } - public Task> GetLoginsAsync(IUser user, CancellationToken cancellationToken) + public Task GetPhoneNumberConfirmedAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.GetLoginsAsync((WrappedIdentityUser)user, cancellationToken); + return Task.FromResult(((MongoUser)user).PhoneNumberConfirmed); } - public Task GetSecurityStampAsync(IUser user, CancellationToken cancellationToken) + public Task GetTwoFactorEnabledAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.GetSecurityStampAsync((WrappedIdentityUser)user, cancellationToken); + return Task.FromResult(((MongoUser)user).TwoFactorEnabled); } - public Task SetSecurityStampAsync(IUser user, string stamp, CancellationToken cancellationToken) + public Task GetLockoutEndDateAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.SetSecurityStampAsync((WrappedIdentityUser)user, stamp, cancellationToken); + return Task.FromResult(((MongoUser)user).LockoutEndDateUtc); } - public Task GetEmailAsync(IUser user, CancellationToken cancellationToken) + public Task GetAccessFailedCountAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.GetEmailAsync((WrappedIdentityUser)user, cancellationToken); + return Task.FromResult(((MongoUser)user).AccessFailedCount); } - public Task SetEmailAsync(IUser user, string email, CancellationToken cancellationToken) + public Task GetLockoutEnabledAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.SetEmailAsync((WrappedIdentityUser)user, email, cancellationToken); + return Task.FromResult(((MongoUser)user).LockoutEnabled); } - public Task GetEmailConfirmedAsync(IUser user, CancellationToken cancellationToken) + public Task GetTokenAsync(IUser user, string loginProvider, string name, CancellationToken cancellationToken) { - return innerStore.GetEmailConfirmedAsync((WrappedIdentityUser)user, cancellationToken); + return Task.FromResult(((MongoUser)user).GetToken(loginProvider, name)); } - public Task SetEmailConfirmedAsync(IUser user, bool confirmed, CancellationToken cancellationToken) + public Task HasPasswordAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.SetEmailConfirmedAsync((WrappedIdentityUser)user, confirmed, cancellationToken); + return Task.FromResult(!string.IsNullOrWhiteSpace(((MongoUser)user).PasswordHash)); } - public Task GetNormalizedEmailAsync(IUser user, CancellationToken cancellationToken) + public Task SetUserNameAsync(IUser user, string userName, CancellationToken cancellationToken) { - return innerStore.GetNormalizedEmailAsync((WrappedIdentityUser)user, cancellationToken); + ((MongoUser)user).UserName = userName; + + return TaskHelper.Done; } - public Task SetNormalizedEmailAsync(IUser user, string normalizedEmail, CancellationToken cancellationToken) + public Task SetNormalizedUserNameAsync(IUser user, string normalizedName, CancellationToken cancellationToken) { - return innerStore.SetNormalizedEmailAsync((WrappedIdentityUser)user, normalizedEmail, cancellationToken); + ((MongoUser)user).NormalizedUserName = normalizedName; + + return TaskHelper.Done; } - public Task> GetClaimsAsync(IUser user, CancellationToken cancellationToken) + public Task SetPasswordHashAsync(IUser user, string passwordHash, CancellationToken cancellationToken) { - return innerStore.GetClaimsAsync((WrappedIdentityUser)user, cancellationToken); + ((MongoUser)user).PasswordHash = passwordHash; + + return TaskHelper.Done; } - public Task AddClaimsAsync(IUser user, IEnumerable claims, CancellationToken cancellationToken) + public Task AddToRoleAsync(IUser user, string roleName, CancellationToken cancellationToken) { - return innerStore.AddClaimsAsync((WrappedIdentityUser)user, claims, cancellationToken); + ((MongoUser)user).AddRole(roleName); + + return TaskHelper.Done; } - public Task ReplaceClaimAsync(IUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) + public Task RemoveFromRoleAsync(IUser user, string roleName, CancellationToken cancellationToken) { - return innerStore.ReplaceClaimAsync((WrappedIdentityUser)user, claim, newClaim, cancellationToken); + ((MongoUser)user).RemoveRole(roleName); + + return TaskHelper.Done; } - public Task RemoveClaimsAsync(IUser user, IEnumerable claims, CancellationToken cancellationToken) + public Task AddLoginAsync(IUser user, UserLoginInfo login, CancellationToken cancellationToken) { - return innerStore.RemoveClaimsAsync((WrappedIdentityUser)user, claims, cancellationToken); + ((MongoUser)user).AddLogin(login); + + return TaskHelper.Done; } - public Task GetPhoneNumberAsync(IUser user, CancellationToken cancellationToken) + public Task RemoveLoginAsync(IUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) { - return innerStore.GetPhoneNumberAsync((WrappedIdentityUser)user, cancellationToken); + ((MongoUser)user).RemoveLogin(loginProvider, providerKey); + + return TaskHelper.Done; } - public Task SetPhoneNumberAsync(IUser user, string phoneNumber, CancellationToken cancellationToken) + public Task SetSecurityStampAsync(IUser user, string stamp, CancellationToken cancellationToken) { - return innerStore.SetPhoneNumberAsync((WrappedIdentityUser)user, phoneNumber, cancellationToken); + ((MongoUser)user).SecurityStamp = stamp; + + return TaskHelper.Done; } - public Task GetPhoneNumberConfirmedAsync(IUser user, CancellationToken cancellationToken) + public Task SetEmailAsync(IUser user, string email, CancellationToken cancellationToken) { - return innerStore.GetPhoneNumberConfirmedAsync((WrappedIdentityUser)user, cancellationToken); + ((MongoUser)user).Email = email; + + return TaskHelper.Done; } - public Task SetPhoneNumberConfirmedAsync(IUser user, bool confirmed, CancellationToken cancellationToken) + public Task SetEmailConfirmedAsync(IUser user, bool confirmed, CancellationToken cancellationToken) { - return innerStore.SetPhoneNumberConfirmedAsync((WrappedIdentityUser)user, confirmed, cancellationToken); + ((MongoUser)user).EmailConfirmed = confirmed; + + return TaskHelper.Done; } - public Task GetTwoFactorEnabledAsync(IUser user, CancellationToken cancellationToken) + public Task SetNormalizedEmailAsync(IUser user, string normalizedEmail, CancellationToken cancellationToken) { - return innerStore.GetTwoFactorEnabledAsync((WrappedIdentityUser)user, cancellationToken); + ((MongoUser)user).NormalizedEmail = normalizedEmail; + + return TaskHelper.Done; } - public Task SetTwoFactorEnabledAsync(IUser user, bool enabled, CancellationToken cancellationToken) + public Task AddClaimsAsync(IUser user, IEnumerable claims, CancellationToken cancellationToken) { - return innerStore.SetTwoFactorEnabledAsync((WrappedIdentityUser)user, enabled, cancellationToken); + ((MongoUser)user).AddClaims(claims); + + return TaskHelper.Done; } - public Task GetLockoutEndDateAsync(IUser user, CancellationToken cancellationToken) + public Task ReplaceClaimAsync(IUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) { - return innerStore.GetLockoutEndDateAsync((WrappedIdentityUser)user, cancellationToken); + ((MongoUser)user).ReplaceClaim(claim, newClaim); + + return TaskHelper.Done; } - public Task SetLockoutEndDateAsync(IUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) + public Task RemoveClaimsAsync(IUser user, IEnumerable claims, CancellationToken cancellationToken) { - return innerStore.SetLockoutEndDateAsync((WrappedIdentityUser)user, lockoutEnd, cancellationToken); + ((MongoUser)user).RemoveClaims(claims); + + return TaskHelper.Done; } - public Task GetAccessFailedCountAsync(IUser user, CancellationToken cancellationToken) + public Task SetPhoneNumberAsync(IUser user, string phoneNumber, CancellationToken cancellationToken) { - return innerStore.GetAccessFailedCountAsync((WrappedIdentityUser)user, cancellationToken); + ((MongoUser)user).PhoneNumber = phoneNumber; + + return TaskHelper.Done; } - public Task IncrementAccessFailedCountAsync(IUser user, CancellationToken cancellationToken) + public Task SetPhoneNumberConfirmedAsync(IUser user, bool confirmed, CancellationToken cancellationToken) { - return innerStore.IncrementAccessFailedCountAsync((WrappedIdentityUser)user, cancellationToken); + ((MongoUser)user).PhoneNumberConfirmed = confirmed; + + return TaskHelper.Done; } - public Task ResetAccessFailedCountAsync(IUser user, CancellationToken cancellationToken) + public Task SetTwoFactorEnabledAsync(IUser user, bool enabled, CancellationToken cancellationToken) { - return innerStore.ResetAccessFailedCountAsync((WrappedIdentityUser)user, cancellationToken); + ((MongoUser)user).TwoFactorEnabled = enabled; + + return TaskHelper.Done; } - public Task GetLockoutEnabledAsync(IUser user, CancellationToken cancellationToken) + public Task SetLockoutEndDateAsync(IUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) { - return innerStore.GetLockoutEnabledAsync((WrappedIdentityUser)user, cancellationToken); + ((MongoUser)user).LockoutEndDateUtc = lockoutEnd?.UtcDateTime; + + return TaskHelper.Done; } - public Task SetLockoutEnabledAsync(IUser user, bool enabled, CancellationToken cancellationToken) + public Task IncrementAccessFailedCountAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.SetLockoutEnabledAsync((WrappedIdentityUser)user, enabled, cancellationToken); + ((MongoUser)user).AccessFailedCount++; + + return Task.FromResult(((MongoUser)user).AccessFailedCount); } - public Task SetTokenAsync(IUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) + public Task ResetAccessFailedCountAsync(IUser user, CancellationToken cancellationToken) { - return innerStore.SetTokenAsync((WrappedIdentityUser)user, loginProvider, name, value, cancellationToken); + ((MongoUser)user).AccessFailedCount = 0; + + return TaskHelper.Done; } - public Task RemoveTokenAsync(IUser user, string loginProvider, string name, CancellationToken cancellationToken) + public Task SetLockoutEnabledAsync(IUser user, bool enabled, CancellationToken cancellationToken) { - return innerStore.RemoveTokenAsync((WrappedIdentityUser)user, loginProvider, name, cancellationToken); + ((MongoUser)user).LockoutEnabled = enabled; + + return TaskHelper.Done; } - public Task GetTokenAsync(IUser user, string loginProvider, string name, CancellationToken cancellationToken) + public Task SetTokenAsync(IUser user, string loginProvider, string name, string value, CancellationToken cancellationToken) { - return innerStore.GetTokenAsync((WrappedIdentityUser)user, loginProvider, name, cancellationToken); + ((MongoUser)user).SetToken(loginProvider, name, value); + + return TaskHelper.Done; } - public Task HasPasswordAsync(IUser user, CancellationToken cancellationToken) + public Task RemoveTokenAsync(IUser user, string loginProvider, string name, CancellationToken cancellationToken) { - return Task.FromResult(!string.IsNullOrWhiteSpace(((WrappedIdentityUser)user).PasswordHash)); + ((MongoUser)user).RemoveToken(loginProvider, name); + + return TaskHelper.Done; } } } diff --git a/src/Squidex.Domain.Users.MongoDb/MongoUserToken.cs b/src/Squidex.Domain.Users.MongoDb/MongoUserToken.cs new file mode 100644 index 000000000..e162116a8 --- /dev/null +++ b/src/Squidex.Domain.Users.MongoDb/MongoUserToken.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// MongoUserToken.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using MongoDB.Bson.Serialization.Attributes; + +namespace Squidex.Domain.Users.MongoDb +{ + public sealed class MongoUserToken + { + [BsonRequired] + [BsonElement] + public string LoginProvider { get; set; } + + [BsonIgnoreIfDefault] + [BsonElement] + public string Name { get; set; } + + [BsonRequired] + [BsonElement] + public string Value { get; set; } + } +} diff --git a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj index c118300b3..31704faa2 100644 --- a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -15,7 +15,6 @@ - diff --git a/src/Squidex.Domain.Users.MongoDb/WrappedIdentityRole.cs b/src/Squidex.Domain.Users.MongoDb/WrappedIdentityRole.cs deleted file mode 100644 index fee399a23..000000000 --- a/src/Squidex.Domain.Users.MongoDb/WrappedIdentityRole.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// WrappedIdentityRole.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using Microsoft.AspNetCore.Identity.MongoDB; - -namespace Squidex.Domain.Users.MongoDb -{ - public sealed class WrappedIdentityRole : IdentityRole, IRole - { - } -} diff --git a/src/Squidex.Domain.Users.MongoDb/WrappedIdentityUser.cs b/src/Squidex.Domain.Users.MongoDb/WrappedIdentityUser.cs deleted file mode 100644 index abbce010a..000000000 --- a/src/Squidex.Domain.Users.MongoDb/WrappedIdentityUser.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// WrappedIdentityUser.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using Microsoft.AspNetCore.Identity.MongoDB; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Users.MongoDb -{ - public sealed class WrappedIdentityUser : IdentityUser, IUser - { - public bool IsLocked - { - get { return LockoutEndDateUtc != null && LockoutEndDateUtc.Value > DateTime.UtcNow; } - } - - IReadOnlyList IUser.Claims - { - get { return Claims.Select(x => new Claim(x.Type, x.Value)).ToList(); } - } - - IReadOnlyList IUser.Logins - { - get { return Logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList(); } - } - - public void UpdateEmail(string email) - { - Email = UserName = email; - } - - public void SetClaim(string type, string value) - { - Claims.RemoveAll(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); - Claims.Add(new IdentityUserClaim { Type = type, Value = value }); - } - } -} diff --git a/src/Squidex.Domain.Users/UserManagerExtensions.cs b/src/Squidex.Domain.Users/UserManagerExtensions.cs index c207d6c7e..799b9a2ee 100644 --- a/src/Squidex.Domain.Users/UserManagerExtensions.cs +++ b/src/Squidex.Domain.Users/UserManagerExtensions.cs @@ -87,7 +87,8 @@ namespace Squidex.Domain.Users if (!string.IsNullOrWhiteSpace(email)) { - user.UpdateEmail(email); + await DoChecked(() => userManager.SetEmailAsync(user, email), "Cannot update email."); + await DoChecked(() => userManager.SetUserNameAsync(user, email), "Cannot update email."); } if (!string.IsNullOrWhiteSpace(displayName)) diff --git a/src/Squidex.Infrastructure/CollectionExtensions.cs b/src/Squidex.Infrastructure/CollectionExtensions.cs index 0b177c816..b26ef92bb 100644 --- a/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -128,5 +128,13 @@ namespace Squidex.Infrastructure return result; } + + public static void Foreach(this IEnumerable collection, Action action) + { + foreach (var item in collection) + { + action(item); + } + } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/DomainForbiddenException.cs b/src/Squidex.Infrastructure/DomainForbiddenException.cs new file mode 100644 index 000000000..13a838583 --- /dev/null +++ b/src/Squidex.Infrastructure/DomainForbiddenException.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// DomainForbiddenException.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure +{ + public class DomainForbiddenException : DomainException + { + public DomainForbiddenException(string message) + : base(message) + { + } + + public DomainForbiddenException(string message, Exception inner) + : base(message, inner) + { + } + } +} diff --git a/src/Squidex/Config/Identity/MicrosoftIdentityUsage.cs b/src/Squidex/Config/Identity/MicrosoftIdentityUsage.cs index 40088d10b..60690d3f7 100644 --- a/src/Squidex/Config/Identity/MicrosoftIdentityUsage.cs +++ b/src/Squidex/Config/Identity/MicrosoftIdentityUsage.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using Microsoft.AspNetCore.Authentication.MicrosoftAccount; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; diff --git a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs index d1d8489c8..c04254de8 100644 --- a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs +++ b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Squidex.Controllers.Api; @@ -24,19 +25,15 @@ namespace Squidex.Pipeline private static void AddHandler(Func handler) where T : Exception { - Handlers.Add(ex => - { - var typed = ex as T; - - return typed != null ? handler(typed) : null; - }); + Handlers.Add(ex => ex is T typed ? handler(typed) : null); } static ApiExceptionFilterAttribute() { - AddHandler(OnDomainObjectNotFoundException); - AddHandler(OnDomainObjectVersionException); AddHandler(OnDomainException); + AddHandler(OnDomainForbiddenException); + AddHandler(OnDomainObjectVersionException); + AddHandler(OnDomainObjectNotFoundException); AddHandler(OnValidationException); } @@ -55,6 +52,11 @@ namespace Squidex.Pipeline return ErrorResult(400, new ErrorDto { Message = ex.Message }); } + private static IActionResult OnDomainForbiddenException(DomainForbiddenException ex) + { + return ErrorResult(401, new ErrorDto { Message = ex.Message }); + } + private static IActionResult OnValidationException(ValidationException ex) { return ErrorResult(400, new ErrorDto { Message = ex.Message, Details = ex.Errors.Select(e => e.Message).ToArray() }); diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index f6a54444f..ac910196a 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -1,13 +1,13 @@  - Squidex true + true $(NoWarn);CS1591;1591;1573;1572 - Exe Squidex true netcoreapp2.0 + Latest true @@ -62,13 +62,16 @@ + + - + + diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JurassicScriptEngineTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JurassicScriptEngineTests.cs new file mode 100644 index 000000000..7e9b06343 --- /dev/null +++ b/tests/Squidex.Domain.Apps.Core.Tests/Scripting/JurassicScriptEngineTests.cs @@ -0,0 +1,135 @@ +// ========================================================================== +// JurassicScriptEngineTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public class JurassicScriptEngineTests + { + private readonly JurassicScriptEngine scriptEngine = + new JurassicScriptEngine( + new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + + [Fact] + public void Should_throw_validation_exception_when_calling_reject() + { + Assert.Throws(() => scriptEngine.Execute(new ScriptContext(), "reject()", "update")); + Assert.Throws(() => scriptEngine.Execute(new ScriptContext(), "reject('Not valid')", "update")); + } + + [Fact] + public void Should_throw_security_exception_when_calling_reject() + { + Assert.Throws(() => scriptEngine.Execute(new ScriptContext(), "disallow()", "Update")); + Assert.Throws(() => scriptEngine.Execute(new ScriptContext(), "disallow('Not allowed')", "update")); + } + + [Fact] + public void Should_catch_script_syntax_errors() + { + Assert.Throws(() => scriptEngine.Execute(new ScriptContext(), "x => x", "update")); + } + + [Fact] + public void Should_catch_script_runtime_errors() + { + Assert.Throws(() => scriptEngine.Execute(new ScriptContext(), "throw 'Error';", "update")); + } + + [Fact] + public void Should_catch_script_runtime_errors_on_transform() + { + Assert.Throws(() => scriptEngine.ExecuteAndTransform(new ScriptContext(), "throw 'Error';", "update", true)); + } + + [Fact] + public void Should_return_original_content_when_script_failed() + { + var content = new NamedContentData(); + var context = new ScriptContext { Data = content }; + + var result = scriptEngine.ExecuteAndTransform(context, "x => x", "update"); + + Assert.Same(content, result); + } + + [Fact] + public void Should_return_original_content_when_content_is_not_replaced() + { + var content = new NamedContentData(); + var context = new ScriptContext { Data = content }; + + var result = scriptEngine.ExecuteAndTransform(context, "var x = 0;", "update"); + + Assert.Same(content, result); + } + + [Fact] + public void Should_returning_empty_content_when_replacing_with_invalid_content() + { + var content = + new NamedContentData() + .AddField("number0", + new ContentFieldData() + .AddValue("iv", 1)) + .AddField("number1", + new ContentFieldData() + .AddValue("iv", 1)); + + var context = new ScriptContext { Data = content }; + + var result = scriptEngine.ExecuteAndTransform(context, @"replace({ test: 1 });", "update"); + + Assert.Equal(new NamedContentData(), result); + } + + [Fact] + public void Should_transform_content_and_return() + { + var content = + new NamedContentData() + .AddField("number0", + new ContentFieldData() + .AddValue("iv", 1)) + .AddField("number1", + new ContentFieldData() + .AddValue("iv", 1)); + var expected = + new NamedContentData() + .AddField("number1", + new ContentFieldData() + .AddValue("iv", 2)) + .AddField("number2", + new ContentFieldData() + .AddValue("iv", 10)); + + var context = new ScriptContext { Data = content }; + + var result = scriptEngine.ExecuteAndTransform(context, @" + var data = ctx.data; + + delete data.number0; + + data.number1.iv = data.number1.iv + 1; + data.number2 = { 'iv': 10 }; + + replace(data);", "update"); + + Assert.Equal(expected, result); + + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index d99b3ad5f..a41525658 100644 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -13,8 +13,8 @@ - - + +