Browse Source

Feature/letsencrypt (#591)

* Batch update for elastic.

* Cleanup

* Fix bulk updates.

* Lets encrypt
pull/593/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
d3f77b5761
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs
  2. 2
      backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupRestoreState.cs
  3. 23
      backend/src/Squidex.Domain.Users.MongoDb/MongoKey.cs
  4. 64
      backend/src/Squidex.Domain.Users.MongoDb/MongoKeyParameters.cs
  5. 29
      backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs
  6. 108
      backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs
  7. 20
      backend/src/Squidex.Domain.Users.MongoDb/MongoXmlEntity.cs
  8. 51
      backend/src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs
  9. 59
      backend/src/Squidex.Domain.Users/DefaultCertificateAccountStore.cs
  10. 73
      backend/src/Squidex.Domain.Users/DefaultCertificateStore.cs
  11. 49
      backend/src/Squidex.Domain.Users/DefaultKeyStore.cs
  12. 69
      backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs
  13. 2
      backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  14. 14
      backend/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs
  15. 3
      backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs
  16. 4
      backend/src/Squidex.Web/UrlsOptions.cs
  17. 28
      backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs
  18. 6
      backend/src/Squidex/Config/Domain/StoreServices.cs
  19. 10
      backend/src/Squidex/Config/Orleans/OrleansServices.cs
  20. 33
      backend/src/Squidex/Config/Web/WebServices.cs
  21. 1
      backend/src/Squidex/Startup.cs
  22. 14
      backend/src/Squidex/appsettings.json
  23. 51
      backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateAccountStoreTests.cs
  24. 72
      backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateStoreTests.cs
  25. 62
      backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs
  26. 61
      backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs

4
backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs

@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
private readonly IServiceProvider serviceProvider;
private readonly IStreamNameResolver streamNameResolver;
private readonly IUserResolver userResolver;
private readonly IGrainState<RestoreState2> state;
private readonly IGrainState<BackupRestoreState> state;
private RestoreContext restoreContext;
private RestoreJob CurrentJob
@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
ICommandBus commandBus,
IEventDataFormatter eventDataFormatter,
IEventStore eventStore,
IGrainState<RestoreState2> state,
IGrainState<BackupRestoreState> state,
IServiceProvider serviceProvider,
IStreamNameResolver streamNameResolver,
IUserResolver userResolver,

2
backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreState2.cs → backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupRestoreState.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Backup.State
{
public class RestoreState2
public class BackupRestoreState
{
public RestoreJob Job { get; set; }
}

23
backend/src/Squidex.Domain.Users.MongoDb/MongoKey.cs

@ -1,23 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using MongoDB.Bson.Serialization.Attributes;
namespace Squidex.Domain.Users.MongoDb
{
public sealed class MongoKey
{
[BsonId]
public string Id { get; set; }
[BsonElement]
public string Key { get; set; }
[BsonElement]
public MongoKeyParameters Parameters { get; set; }
}
}

64
backend/src/Squidex.Domain.Users.MongoDb/MongoKeyParameters.cs

@ -1,64 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security.Cryptography;
#pragma warning disable IDE0017 // Simplify object initialization
namespace Squidex.Domain.Users.MongoDb
{
public sealed class MongoKeyParameters
{
public byte[] D { get; set; }
public byte[] DP { get; set; }
public byte[] DQ { get; set; }
public byte[] Exponent { get; set; }
public byte[] InverseQ { get; set; }
public byte[] Modulus { get; set; }
public byte[] P { get; set; }
public byte[] Q { get; set; }
public static MongoKeyParameters Create(RSAParameters source)
{
var mongoParameters = new MongoKeyParameters();
mongoParameters.D = source.D;
mongoParameters.DP = source.DP;
mongoParameters.DQ = source.DQ;
mongoParameters.Exponent = source.Exponent;
mongoParameters.InverseQ = source.InverseQ;
mongoParameters.Modulus = source.Modulus;
mongoParameters.P = source.P;
mongoParameters.Q = source.Q;
return mongoParameters;
}
public RSAParameters ToParameters()
{
var parameters = default(RSAParameters);
parameters.D = D;
parameters.DP = DP;
parameters.DQ = DQ;
parameters.Exponent = Exponent;
parameters.InverseQ = InverseQ;
parameters.Modulus = Modulus;
parameters.P = P;
parameters.Q = Q;
return parameters;
}
}
}

29
backend/src/Squidex.Domain.Users.MongoDb/MongoUser.cs

@ -9,23 +9,32 @@ using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Infrastructure;
namespace Squidex.Domain.Users.MongoDb
{
public sealed class MongoUser : IdentityUser
{
[BsonRequired]
[BsonElement]
public List<Claim> Claims { get; set; } = new List<Claim>();
[BsonRequired]
[BsonElement]
public List<UserTokenInfo> Tokens { get; set; } = new List<UserTokenInfo>();
[BsonRequired]
[BsonElement]
public List<UserLoginInfo> Logins { get; set; } = new List<UserLoginInfo>();
[BsonRequired]
[BsonElement]
public HashSet<string> Roles { get; set; } = new HashSet<string>();
internal void AddLogin(UserLoginInfo login)
{
Logins.Add(new UserLoginInfo(login.LoginProvider, login.ProviderKey, login.ProviderDisplayName));
Logins.Add(login);
}
internal void AddRole(string role)
@ -38,9 +47,9 @@ namespace Squidex.Domain.Users.MongoDb
Roles.Remove(role);
}
internal void RemoveLogin(string loginProvider, string providerKey)
internal void RemoveLogin(string provider, string providerKey)
{
Logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey);
Logins.RemoveAll(x => x.LoginProvider == provider && x.ProviderKey == providerKey);
}
internal void AddClaim(Claim claim)
@ -55,7 +64,7 @@ namespace Squidex.Domain.Users.MongoDb
internal void RemoveClaim(Claim claim)
{
Claims.RemoveAll(c => c.Type == claim.Type && c.Value == claim.Value);
Claims.RemoveAll(x => x.Type == claim.Type && x.Value == claim.Value);
}
internal void RemoveClaims(IEnumerable<Claim> claims)
@ -63,19 +72,19 @@ namespace Squidex.Domain.Users.MongoDb
claims.Foreach((x, _) => RemoveClaim(x));
}
internal string? GetToken(string loginProvider, string name)
internal string? GetToken(string provider, string name)
{
return Tokens.FirstOrDefault(t => t.LoginProvider == loginProvider && t.Name == name)?.Value;
return Tokens.FirstOrDefault(x => x.LoginProvider == provider && x.Name == name)?.Value;
}
internal void AddToken(string loginProvider, string name, string value)
internal void AddToken(string provider, string name, string value)
{
Tokens.Add(new UserTokenInfo { LoginProvider = loginProvider, Name = name, Value = value });
Tokens.Add(new UserTokenInfo { LoginProvider = provider, Name = name, Value = value });
}
internal void RemoveToken(string loginProvider, string name)
internal void RemoveToken(string provider, string name)
{
Tokens.RemoveAll(t => t.LoginProvider == loginProvider && t.Name == name);
Tokens.RemoveAll(x => x.LoginProvider == provider && x.Name == name);
}
internal void ReplaceClaim(Claim existingClaim, Claim newClaim)

108
backend/src/Squidex.Domain.Users.MongoDb/MongoUserStore.cs

@ -193,27 +193,37 @@ namespace Squidex.Domain.Users.MongoDb
public async Task<IdentityUser> FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken)
{
return await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken);
var result = await Collection.Find(x => x.NormalizedEmail == normalizedEmail).FirstOrDefaultAsync(cancellationToken);
return result;
}
public async Task<IdentityUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
{
return await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken);
var result = await Collection.Find(x => x.NormalizedEmail == normalizedUserName).FirstOrDefaultAsync(cancellationToken);
return result;
}
public async Task<IdentityUser> FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken)
{
return await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken);
var result = await Collection.Find(x => x.Logins.Any(y => y.LoginProvider == loginProvider && y.ProviderKey == providerKey)).FirstOrDefaultAsync(cancellationToken);
return result;
}
public async Task<IList<IdentityUser>> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken)
{
return (await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken)).OfType<IdentityUser>().ToList();
var result = await Collection.Find(x => x.Claims.Any(y => y.Type == claim.Type && y.Value == claim.Value)).ToListAsync(cancellationToken);
return result.OfType<IdentityUser>().ToList();
}
public async Task<IList<IdentityUser>> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken)
{
return (await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken)).OfType<IdentityUser>().ToList();
var result = await Collection.Find(x => x.Roles.Contains(roleName)).ToListAsync(cancellationToken);
return result.OfType<IdentityUser>().ToList();
}
public async Task<IdentityResult> CreateAsync(IdentityUser user, CancellationToken cancellationToken)
@ -241,112 +251,156 @@ namespace Squidex.Domain.Users.MongoDb
public Task<string> GetUserIdAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).Id);
var result = user.Id;
return Task.FromResult(result);
}
public Task<string> GetUserNameAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).UserName);
var result = user.UserName;
return Task.FromResult(result);
}
public Task<string> GetNormalizedUserNameAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).NormalizedUserName);
var result = user.NormalizedUserName;
return Task.FromResult(result);
}
public Task<string> GetPasswordHashAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).PasswordHash);
var result = user.PasswordHash;
return Task.FromResult(result);
}
public Task<IList<string>> GetRolesAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult<IList<string>>(((MongoUser)user).Roles.ToList());
var result = ((MongoUser)user).Roles.ToList();
return Task.FromResult<IList<string>>(result);
}
public Task<bool> IsInRoleAsync(IdentityUser user, string roleName, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).Roles.Contains(roleName));
var result = ((MongoUser)user).Roles.Contains(roleName);
return Task.FromResult(result);
}
public Task<IList<UserLoginInfo>> GetLoginsAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult<IList<UserLoginInfo>>(((MongoUser)user).Logins.Select(x => new UserLoginInfo(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList());
var result = ((MongoUser)user).Logins.Select(x => new UserLoginInfo(x.LoginProvider, x.ProviderKey, x.ProviderDisplayName)).ToList();
return Task.FromResult<IList<UserLoginInfo>>(result);
}
public Task<string> GetSecurityStampAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).SecurityStamp);
var result = user.SecurityStamp;
return Task.FromResult(result);
}
public Task<string> GetEmailAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).Email);
var result = user.Email;
return Task.FromResult(result);
}
public Task<bool> GetEmailConfirmedAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).EmailConfirmed);
var result = user.EmailConfirmed;
return Task.FromResult(result);
}
public Task<string> GetNormalizedEmailAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).NormalizedEmail);
var result = user.NormalizedEmail;
return Task.FromResult(result);
}
public Task<IList<Claim>> GetClaimsAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult<IList<Claim>>(((MongoUser)user).Claims);
var result = ((MongoUser)user).Claims;
return Task.FromResult<IList<Claim>>(result);
}
public Task<string> GetPhoneNumberAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).PhoneNumber);
var result = user.PhoneNumber;
return Task.FromResult(result);
}
public Task<bool> GetPhoneNumberConfirmedAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).PhoneNumberConfirmed);
var result = user.PhoneNumberConfirmed;
return Task.FromResult(result);
}
public Task<bool> GetTwoFactorEnabledAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).TwoFactorEnabled);
var result = user.TwoFactorEnabled;
return Task.FromResult(result);
}
public Task<DateTimeOffset?> GetLockoutEndDateAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).LockoutEnd);
var result = user.LockoutEnd;
return Task.FromResult(result);
}
public Task<int> GetAccessFailedCountAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).AccessFailedCount);
var result = user.AccessFailedCount;
return Task.FromResult(result);
}
public Task<bool> GetLockoutEnabledAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).LockoutEnabled);
var result = user.LockoutEnabled;
return Task.FromResult(result);
}
public Task<string> GetTokenAsync(IdentityUser user, string loginProvider, string name, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).GetToken(loginProvider, name)!);
var result = ((MongoUser)user).GetToken(loginProvider, name)!;
return Task.FromResult(result);
}
public Task<string> GetAuthenticatorKeyAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)!);
var result = ((MongoUser)user).GetToken(InternalLoginProvider, AuthenticatorKeyTokenName)!;
return Task.FromResult(result);
}
public Task<bool> HasPasswordAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(!string.IsNullOrWhiteSpace(((MongoUser)user).PasswordHash));
var result = !string.IsNullOrWhiteSpace(user.PasswordHash);
return Task.FromResult(result);
}
public Task<int> CountCodesAsync(IdentityUser user, CancellationToken cancellationToken)
{
return Task.FromResult(((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName)?.Split(';').Length ?? 0);
var result = ((MongoUser)user).GetToken(InternalLoginProvider, RecoveryCodeTokenName)?.Split(';').Length ?? 0;
return Task.FromResult(result);
}
public Task SetUserNameAsync(IdentityUser user, string userName, CancellationToken cancellationToken)

20
backend/src/Squidex.Domain.Users.MongoDb/MongoXmlEntity.cs

@ -1,20 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using MongoDB.Bson.Serialization.Attributes;
namespace Squidex.Domain.Users.MongoDb
{
public sealed class MongoXmlEntity
{
[BsonId]
public string FriendlyName { get; set; }
[BsonRequired]
public string Xml { get; set; }
}
}

51
backend/src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs

@ -1,51 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection.Repositories;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Infrastructure;
namespace Squidex.Domain.Users.MongoDb
{
public sealed class MongoXmlRepository : IXmlRepository
{
private static readonly ReplaceOptions UpsertReplace = new ReplaceOptions { IsUpsert = true };
private readonly IMongoCollection<MongoXmlEntity> collection;
public MongoXmlRepository(IMongoDatabase mongoDatabase)
{
Guard.NotNull(mongoDatabase, nameof(mongoDatabase));
collection = mongoDatabase.GetCollection<MongoXmlEntity>("States_Repository");
}
public IReadOnlyCollection<XElement> GetAllElements()
{
var documents = collection.Find(new BsonDocument()).ToList();
var elements = documents.Select(x => XElement.Parse(x.Xml)).ToList();
return elements;
}
public void StoreElement(XElement element, string friendlyName)
{
var document = new MongoXmlEntity
{
FriendlyName = friendlyName
};
document.Xml = element.ToString();
collection.ReplaceOne(x => x.FriendlyName == friendlyName, document, UpsertReplace);
}
}
}

59
backend/src/Squidex.Domain.Users/DefaultCertificateAccountStore.cs

@ -0,0 +1,59 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using LettuceEncrypt.Accounts;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Users
{
public sealed class DefaultCertificateAccountStore : IAccountStore
{
private readonly ISnapshotStore<State, Guid> store;
[CollectionName("Identity_certificateAccount")]
public sealed class State
{
public AccountModel Account { get; set; }
public State()
{
}
public State(AccountModel account)
{
Account = account;
}
}
public DefaultCertificateAccountStore(ISnapshotStore<State, Guid> store)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public async Task<AccountModel?> GetAccountAsync(CancellationToken cancellationToken)
{
var (value, _) = await store.ReadAsync(default);
return value?.Account;
}
public Task SaveAccountAsync(AccountModel account, CancellationToken cancellationToken)
{
Guard.NotNull(account, nameof(account));
var state = new State(account);
return store.WriteAsync(default, state, EtagVersion.Any, 0);
}
}
}

73
backend/src/Squidex.Domain.Users/DefaultCertificateStore.cs

@ -0,0 +1,73 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using LettuceEncrypt;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Users
{
public sealed class DefaultCertificateStore : ICertificateRepository, ICertificateSource
{
private readonly ISnapshotStore<State, Guid> store;
[CollectionName("Identity_Certificates")]
public sealed class State
{
public byte[] Certificate { get; set; }
public State()
{
}
public State(X509Certificate2 certificate)
{
Certificate = certificate.Export(X509ContentType.Pfx);
}
public X509Certificate2 ToCertificate()
{
return new X509Certificate2(Certificate);
}
}
public DefaultCertificateStore(ISnapshotStore<State, Guid> store)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public async Task<IEnumerable<X509Certificate2>> GetCertificatesAsync(CancellationToken cancellationToken = default)
{
var result = new List<X509Certificate2>();
await store.ReadAllAsync((state, _) =>
{
result.Add(state.ToCertificate());
return Task.CompletedTask;
}, cancellationToken);
return result;
}
public Task SaveAsync(X509Certificate2 certificate, CancellationToken cancellationToken = default)
{
Guard.NotNull(certificate, nameof(certificate));
var state = new State(certificate);
return store.WriteAsync(Guid.NewGuid(), state, EtagVersion.Any, 0);
}
}
}

49
backend/src/Squidex.Domain.Users.MongoDb/MongoKeyStore.cs → backend/src/Squidex.Domain.Users/DefaultKeyStore.cs

@ -13,30 +13,32 @@ using IdentityModel;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.IdentityModel.Tokens;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Users.MongoDb
namespace Squidex.Domain.Users
{
public sealed class MongoKeyStore : MongoRepositoryBase<MongoKey>, ISigningCredentialStore, IValidationKeysStore
public sealed class DefaultKeyStore : ISigningCredentialStore, IValidationKeysStore
{
private readonly ISnapshotStore<State, Guid> store;
private SigningCredentials? cachedKey;
private SecurityKeyInfo[]? cachedKeyInfo;
public MongoKeyStore(IMongoDatabase database, bool setup = false)
: base(database, setup)
[CollectionName("Identity_Keys")]
public sealed class State
{
public string Key { get; set; }
public RSAParameters Parameters { get; set; }
}
protected override string CollectionName()
public DefaultKeyStore(ISnapshotStore<State, Guid> store)
{
return "Key";
this.store = store;
}
public async Task<SigningCredentials> GetSigningCredentialsAsync()
{
var (_, key) = await GetOrCreateKeyAsync();
// SignatureProvider signatureProvider = key.CryptoProviderFactory.CreateForVerifying(key, key.Al);
return key;
}
@ -55,57 +57,50 @@ namespace Squidex.Domain.Users.MongoDb
return (cachedKeyInfo, cachedKey);
}
var key = await Collection.Find(x => x.Id == "Default").FirstOrDefaultAsync();
var (state, _) = await store.ReadAsync(default);
RsaSecurityKey securityKey;
if (key == null)
if (state == null)
{
securityKey = new RsaSecurityKey(RSA.Create(2048))
{
KeyId = CryptoRandom.CreateUniqueId(16)
};
key = new MongoKey { Id = "Default", Key = securityKey.KeyId };
state = new State { Key = securityKey.KeyId };
if (securityKey.Rsa != null)
{
var parameters = securityKey.Rsa.ExportParameters(includePrivateParameters: true);
key.Parameters = MongoKeyParameters.Create(parameters);
state.Parameters = parameters;
}
else
{
key.Parameters = MongoKeyParameters.Create(securityKey.Parameters);
state.Parameters = securityKey.Parameters;
}
try
{
await Collection.InsertOneAsync(key);
await store.WriteAsync(default, state, 0, 0);
return CreateCredentialsPair(securityKey);
}
catch (MongoWriteException ex)
catch (InconsistentStateException)
{
if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
key = await Collection.Find(x => x.Id == "Default").FirstOrDefaultAsync();
}
else
{
throw;
}
(state, _) = await store.ReadAsync(default);
}
}
if (key == null)
if (state == null)
{
throw new InvalidOperationException("Cannot read key.");
}
securityKey = new RsaSecurityKey(key.Parameters.ToParameters())
securityKey = new RsaSecurityKey(state.Parameters)
{
KeyId = key.Key
KeyId = state.Key
};
return CreateCredentialsPair(securityKey);

69
backend/src/Squidex.Domain.Users/DefaultXmlRepository.cs

@ -0,0 +1,69 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Users
{
public sealed class DefaultXmlRepository : IXmlRepository
{
private readonly ISnapshotStore<State, string> store;
[CollectionName("Identity_Xml")]
public sealed class State
{
public string Xml { get; set; }
public State()
{
}
public State(XElement xml)
{
Xml = xml.ToString();
}
public XElement ToXml()
{
return XElement.Parse(Xml);
}
}
public DefaultXmlRepository(ISnapshotStore<State, string> store)
{
Guard.NotNull(store, nameof(store));
this.store = store;
}
public IReadOnlyCollection<XElement> GetAllElements()
{
var result = new List<XElement>();
store.ReadAllAsync((state, _) =>
{
result.Add(state.ToXml());
return Task.CompletedTask;
}).Wait();
return result;
}
public void StoreElement(XElement element, string friendlyName)
{
var state = new State(element);
store.WriteAsync(friendlyName, state, EtagVersion.Any, 0);
}
}
}

2
backend/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -16,6 +16,8 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="4.1.1" />
<PackageReference Include="LettuceEncrypt" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="3.1.8" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.7.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

14
backend/src/Squidex.Infrastructure/DependencyInjection/DependencyInjectionExtensions.cs

@ -92,5 +92,19 @@ namespace Microsoft.Extensions.DependencyInjection
return new InterfaceRegistrator<T>((t, f) => services.AddSingleton(t, f), services.TryAddSingleton);
}
public static InterfaceRegistrator<T> AddScopedAs<T>(this IServiceCollection services, Func<IServiceProvider, T> factory) where T : class
{
services.AddScoped(typeof(T), factory);
return new InterfaceRegistrator<T>((t, f) => services.AddScoped(t, f), services.TryAddScoped);
}
public static InterfaceRegistrator<T> AddScopedAs<T>(this IServiceCollection services) where T : class
{
services.AddScoped<T, T>();
return new InterfaceRegistrator<T>((t, f) => services.AddScoped(t, f), services.TryAddScoped);
}
}
}

3
backend/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs

@ -14,7 +14,8 @@ namespace Squidex.Infrastructure.Log.Adapter
{
public static ILoggingBuilder AddSemanticLog(this ILoggingBuilder builder)
{
builder.Services.AddSingleton<ILoggerProvider, SemanticLogLoggerProvider>();
builder.Services.AddSingletonAs<SemanticLogLoggerProvider>()
.As<ILoggerProvider>();
return builder;
}

4
backend/src/Squidex.Web/UrlsOptions.cs

@ -20,8 +20,12 @@ namespace Squidex.Web
public bool EnableXForwardedHost { get; set; }
public bool EnableLetsEncrypt { get; set; }
public bool EnforceHTTPS { get; set; }
public string Email { get; set; } = "admin@squidex.io";
public string BaseUrl
{
get

28
backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs

@ -27,7 +27,7 @@ namespace Squidex.Areas.IdentityServer.Config
{
public static void AddSquidexIdentityServer(this IServiceCollection services)
{
services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(s =>
services.AddSingletonAs<IConfigureOptions<KeyManagementOptions>>(s =>
{
return new ConfigureOptions<KeyManagementOptions>(options =>
{
@ -40,20 +40,26 @@ namespace Squidex.Areas.IdentityServer.Config
services.AddIdentity<IdentityUser, IdentityRole>()
.AddDefaultTokenProviders();
services.AddSingleton<IPasswordValidator<IdentityUser>,
PwnedPasswordValidator>();
services.AddSingletonAs<DefaultXmlRepository>()
.As<IXmlRepository>();
services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>,
UserClaimsPrincipalFactoryWithEmail>();
services.AddSingletonAs<DefaultKeyStore>()
.As<ISigningCredentialStore>().As<IValidationKeysStore>();
services.AddSingleton<IClaimsTransformation,
ApiPermissionUnifier>();
services.AddSingletonAs<PwnedPasswordValidator>()
.As<IPasswordValidator<IdentityUser>>();
services.AddSingleton<IClientStore,
LazyClientStore>();
services.AddScopedAs<UserClaimsPrincipalFactoryWithEmail>()
.As<IUserClaimsPrincipalFactory<IdentityUser>>();
services.AddSingleton<IResourceStore,
InMemoryResourcesStore>();
services.AddSingletonAs<ApiPermissionUnifier>()
.As<IClaimsTransformation>();
services.AddSingletonAs<LazyClientStore>()
.As<IClientStore>();
services.AddSingletonAs<InMemoryResourcesStore>()
.As<IResourceStore>();
services.AddIdentityServer(options =>
{

6
backend/src/Squidex/Config/Domain/StoreServices.cs

@ -108,12 +108,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<MongoUserStore>()
.As<IUserStore<IdentityUser>>().As<IUserFactory>();
services.AddSingletonAs<MongoXmlRepository>()
.As<IXmlRepository>();
services.AddSingletonAs<MongoKeyStore>()
.As<ISigningCredentialStore>().As<IValidationKeysStore>();
services.AddSingletonAs<MongoAssetRepository>()
.As<IAssetRepository>().As<ISnapshotStore<AssetState, DomainId>>();

10
backend/src/Squidex/Config/Orleans/OrleansServices.cs

@ -32,10 +32,14 @@ namespace Squidex.Config.Orleans
builder.ConfigureServices(siloServices =>
{
siloServices.AddSingleton<IMongoClientFactory, DefaultMongoClientFactory>();
siloServices.AddSingletonAs<DefaultMongoClientFactory>()
.As<IMongoClientFactory>();
siloServices.AddSingleton<IActivationLimiter, ActivationLimiter>();
siloServices.AddScoped<IActivationLimit, ActivationLimit>();
siloServices.AddSingletonAs<ActivationLimiter>()
.As<IActivationLimiter>();
siloServices.AddScopedAs<ActivationLimit>()
.As<IActivationLimit>();
siloServices.AddScoped(typeof(IGrainState<>), typeof(Infrastructure.Orleans.GrainState<>));
});

33
backend/src/Squidex/Config/Web/WebServices.cs

@ -5,6 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using LettuceEncrypt;
using LettuceEncrypt.Accounts;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
@ -14,6 +17,7 @@ using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Squidex.Config.Domain;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Translations;
using Squidex.Pipeline.Plugins;
@ -104,5 +108,34 @@ namespace Squidex.Config.Web
.AddSquidexPlugins(config)
.AddSquidexSerializers();
}
public static void AddSquidexLetsEncrypt(this IServiceCollection services, IConfiguration config)
{
var urlsOptions = config.GetSection("urls").Get<UrlsOptions>();
if (!urlsOptions.EnableLetsEncrypt)
{
return;
}
services.AddLettuceEncrypt(options =>
{
options.AcceptTermsOfService = true;
options.DomainNames = new[]
{
new Uri(urlsOptions.BaseUrl).Host
};
options.EmailAddress = urlsOptions.Email;
});
services.AddSingletonAs<DefaultCertificateStore>()
.As<ICertificateRepository>()
.As<ICertificateSource>();
services.AddSingletonAs<DefaultCertificateAccountStore>()
.As<IAccountStore>();
}
}
}

1
backend/src/Squidex/Startup.cs

@ -41,6 +41,7 @@ namespace Squidex
services.AddNonBreakingSameSiteCookies();
services.AddSquidexMvcWithPlugins(config);
services.AddSquidexLetsEncrypt(config);
services.AddSquidexApps();
services.AddSquidexAssetInfrastructure(config);

14
backend/src/Squidex/appsettings.json

@ -20,7 +20,19 @@
/*
* Set it to true to use the X-Forwarded-Host header as internal Hostname.
*/
"enableXForwardedHost": false
"enableXForwardedHost": false,
/*
* Enable lets encrypt certificate handling.
*
* Attention: Can only be used in production environment with custom domain names and a single server.
*/
"enableLetsEncrypt": true,
/*
* The email address for the certificate.
*/
"email": "admin@squidex.io"
},
"fullText": {

51
backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateAccountStoreTests.cs

@ -0,0 +1,51 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using LettuceEncrypt.Accounts;
using Squidex.Infrastructure.States;
using Xunit;
namespace Squidex.Domain.Users
{
public class DefaultCertificateAccountStoreTests
{
private readonly ISnapshotStore<DefaultCertificateAccountStore.State, Guid> store = A.Fake<ISnapshotStore<DefaultCertificateAccountStore.State, Guid>>();
private readonly DefaultCertificateAccountStore sut;
public DefaultCertificateAccountStoreTests()
{
sut = new DefaultCertificateAccountStore(store);
}
[Fact]
public async Task Should_read_from_store()
{
var model = new AccountModel();
A.CallTo(() => store.ReadAsync(default))
.Returns((new DefaultCertificateAccountStore.State { Account = model }, 0));
var result = await sut.GetAccountAsync(default);
Assert.Same(model, result);
}
[Fact]
public async Task Should_write_to_store()
{
var model = new AccountModel();
await sut.SaveAccountAsync(model, default);
A.CallTo(() => store.WriteAsync(A<Guid>._, A<DefaultCertificateAccountStore.State>._, A<long>._, 0))
.MustHaveHappened();
}
}
}

72
backend/tests/Squidex.Domain.Users.Tests/DefaultCertificateStoreTests.cs

@ -0,0 +1,72 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.States;
using Xunit;
namespace Squidex.Domain.Users
{
public sealed class DefaultCertificateStoreTests
{
private readonly ISnapshotStore<DefaultCertificateStore.State, Guid> store = A.Fake<ISnapshotStore<DefaultCertificateStore.State, Guid>>();
private readonly DefaultCertificateStore sut;
public DefaultCertificateStoreTests()
{
sut = new DefaultCertificateStore(store);
}
[Fact]
public async Task Should_read_from_store()
{
A.CallTo(() => store.ReadAllAsync(A<Func<DefaultCertificateStore.State, long, Task>>._, A<CancellationToken>._))
.Invokes((Func<DefaultCertificateStore.State, long, Task> callback, CancellationToken _) =>
{
callback(new DefaultCertificateStore.State
{
Certificate = MakeCert().Export(X509ContentType.Pfx)
}, 0);
callback(new DefaultCertificateStore.State
{
Certificate = MakeCert().Export(X509ContentType.Pfx)
}, 0);
});
var xml = await sut.GetCertificatesAsync();
Assert.Equal(2, xml.Count());
}
[Fact]
public async Task Should_write_to_store()
{
var certificate = MakeCert();
await sut.SaveAsync(certificate, default);
A.CallTo(() => store.WriteAsync(A<Guid>._, A<DefaultCertificateStore.State>._, A<long>._, 0))
.MustHaveHappened();
}
private static X509Certificate2 MakeCert()
{
var ecdsa = ECDsa.Create();
var certificateRequest = new CertificateRequest("cn=foobar", ecdsa, HashAlgorithmName.SHA256);
return certificateRequest.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5));
}
}
}

62
backend/tests/Squidex.Domain.Users.Tests/DefaultKeyStoreTests.cs

@ -0,0 +1,62 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.States;
using Xunit;
namespace Squidex.Domain.Users
{
public class DefaultKeyStoreTests
{
private readonly ISnapshotStore<DefaultKeyStore.State, Guid> store = A.Fake<ISnapshotStore<DefaultKeyStore.State, Guid>>();
private readonly DefaultKeyStore sut;
public DefaultKeyStoreTests()
{
sut = new DefaultKeyStore(store);
}
[Fact]
public async Task Should_generate_signing_credentials_once()
{
A.CallTo(() => store.ReadAsync(A<Guid>._))
.Returns((null!, 0));
var credentials1 = await sut.GetSigningCredentialsAsync();
var credentials2 = await sut.GetSigningCredentialsAsync();
Assert.Same(credentials1, credentials2);
A.CallTo(() => store.ReadAsync(A<Guid>._))
.MustHaveHappenedOnceExactly();
A.CallTo(() => store.WriteAsync(A<Guid>._, A<DefaultKeyStore.State>._, 0, 0))
.MustHaveHappenedOnceExactly();
}
[Fact]
public async Task Should_generate_validation_keys_once()
{
A.CallTo(() => store.ReadAsync(A<Guid>._))
.Returns((null!, 0));
var credentials1 = await sut.GetValidationKeysAsync();
var credentials2 = await sut.GetValidationKeysAsync();
Assert.Same(credentials1, credentials2);
A.CallTo(() => store.ReadAsync(A<Guid>._))
.MustHaveHappenedOnceExactly();
A.CallTo(() => store.WriteAsync(A<Guid>._, A<DefaultKeyStore.State>._, 0, 0))
.MustHaveHappenedOnceExactly();
}
}
}

61
backend/tests/Squidex.Domain.Users.Tests/DefaultXmlRepositoryTests.cs

@ -0,0 +1,61 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using FakeItEasy;
using Squidex.Infrastructure.States;
using Xunit;
namespace Squidex.Domain.Users
{
public sealed class DefaultXmlRepositoryTests
{
private readonly ISnapshotStore<DefaultXmlRepository.State, string> store = A.Fake<ISnapshotStore<DefaultXmlRepository.State, string>>();
private readonly DefaultXmlRepository sut;
public DefaultXmlRepositoryTests()
{
sut = new DefaultXmlRepository(store);
}
[Fact]
public void Should_read_from_store()
{
A.CallTo(() => store.ReadAllAsync(A< Func<DefaultXmlRepository.State, long, Task>>._, A<CancellationToken>._))
.Invokes((Func<DefaultXmlRepository.State, long, Task> callback, CancellationToken _) =>
{
callback(new DefaultXmlRepository.State
{
Xml = new XElement("a").ToString()
}, 0);
callback(new DefaultXmlRepository.State
{
Xml = new XElement("b").ToString()
}, 0);
});
var xml = sut.GetAllElements();
Assert.Equal(2, xml.Count);
}
[Fact]
public void Should_write_to_store()
{
var xml = new XElement("x");
sut.StoreElement(xml, "name");
A.CallTo(() => store.WriteAsync("name", A<DefaultXmlRepository.State>._, A<long>._, 0))
.MustHaveHappened();
}
}
}
Loading…
Cancel
Save