Browse Source

IDentity improvements

pull/1/head
Sebastian 9 years ago
parent
commit
4116ce506e
  1. 6
      src/Squidex.Core/Identity/SquidexClaimTypes.cs
  2. 21
      src/Squidex.Core/Identity/SquidexRoles.cs
  3. 5
      src/Squidex.Read.MongoDb/Users/MongoUserEntity.cs
  4. 7
      src/Squidex.Read.MongoDb/Users/MongoUserRepository.cs
  5. 2
      src/Squidex.Read/Users/Repositories/IUserRepository.cs
  6. 5
      src/Squidex/Config/Identity/IdentityServices.cs
  7. 5
      src/Squidex/Config/Identity/IdentityUsage.cs
  8. 2
      src/Squidex/Config/Identity/MyIdentityOptions.cs
  9. 3
      src/Squidex/Controllers/Api/Apps/AppClientsController.cs
  10. 3
      src/Squidex/Controllers/Api/Apps/AppContributorsController.cs
  11. 3
      src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs
  12. 3
      src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs
  13. 3
      src/Squidex/Controllers/Api/Schemas/SchemasController.cs
  14. 3
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  15. 3
      src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs
  16. 93
      src/Squidex/Controllers/UI/Account/AccountController.cs
  17. 23
      src/Squidex/Pipeline/AppFilterAttribute.cs
  18. 22
      src/Squidex/Views/Account/LockedOut.cshtml
  19. 1551
      src/Squidex/app/theme/icomoon/selection.json

6
src/Squidex.Infrastructure/Security/ExtendedClaimTypes.cs → src/Squidex.Core/Identity/SquidexClaimTypes.cs

@ -1,14 +1,14 @@
// ==========================================================================
// ExtendedClaimTypes.cs
// SquidexClaimTypes.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.Security
namespace Squidex.Core.Identity
{
public class ExtendedClaimTypes
public class SquidexClaimTypes
{
public const string SquidexDisplayName = "urn:squidex:name";

21
src/Squidex.Core/Identity/SquidexRoles.cs

@ -0,0 +1,21 @@
// ==========================================================================
// SquidexRoles.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Core.Identity
{
public static class SquidexRoles
{
public const string Administrator = "administrator";
public const string AppOwner = "app-owner";
public const string AppEditor = "app-editor";
public const string AppDeveloper = "app-developer";
}
}

5
src/Squidex.Read.MongoDb/Users/MongoUserEntity.cs

@ -7,6 +7,7 @@
// ==========================================================================
using Microsoft.AspNetCore.Identity.MongoDB;
using Squidex.Core.Identity;
using Squidex.Infrastructure.Security;
using Squidex.Read.Users;
@ -28,12 +29,12 @@ namespace Squidex.Read.MongoDb.Users
public string DisplayName
{
get { return inner.Claims.Find(x => x.Type == ExtendedClaimTypes.SquidexDisplayName)?.Value; }
get { return inner.Claims.Find(x => x.Type == SquidexClaimTypes.SquidexDisplayName)?.Value; }
}
public string PictureUrl
{
get { return inner.Claims.Find(x => x.Type == ExtendedClaimTypes.SquidexPictureUrl)?.Value; }
get { return inner.Claims.Find(x => x.Type == SquidexClaimTypes.SquidexPictureUrl)?.Value; }
}
public MongoUserEntity(IdentityUser inner)

7
src/Squidex.Read.MongoDb/Users/MongoUserRepository.cs

@ -41,5 +41,12 @@ namespace Squidex.Read.MongoDb.Users
return user != null ? new MongoUserEntity(user) : null;
}
public Task<long> CountAsync()
{
var count = userManager.Users.LongCount();
return Task.FromResult(count);
}
}
}

2
src/Squidex.Read/Users/Repositories/IUserRepository.cs

@ -16,5 +16,7 @@ namespace Squidex.Read.Users.Repositories
Task<List<IUserEntity>> QueryUsersByQuery(string query);
Task<IUserEntity> FindUserByIdAsync(string id);
Task<long> CountAsync();
}
}

5
src/Squidex/Config/Identity/IdentityServices.cs

@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity.MongoDB;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Core.Identity;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using StackExchange.Redis;
@ -130,8 +131,8 @@ namespace Squidex.Config.Identity
yield return new IdentityResource(Constants.ProfileScope,
new[]
{
ExtendedClaimTypes.SquidexDisplayName,
ExtendedClaimTypes.SquidexPictureUrl
SquidexClaimTypes.SquidexDisplayName,
SquidexClaimTypes.SquidexPictureUrl
});
}
}

5
src/Squidex/Config/Identity/IdentityUsage.cs

@ -18,6 +18,7 @@ using Microsoft.AspNetCore.Identity.MongoDB;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using Squidex.Core.Identity;
using Squidex.Infrastructure.Security;
// ReSharper disable InvertIf
@ -116,7 +117,7 @@ namespace Squidex.Config.Identity
var displayNameClaim = context.Identity.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Name);
if (displayNameClaim != null)
{
context.Identity.AddClaim(new Claim(ExtendedClaimTypes.SquidexDisplayName, displayNameClaim.Value));
context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, displayNameClaim.Value));
}
return base.CreatingTicket(context);
@ -147,7 +148,7 @@ namespace Squidex.Config.Identity
if (!string.IsNullOrWhiteSpace(pictureUrl))
{
context.Identity.AddClaim(new Claim(ExtendedClaimTypes.SquidexPictureUrl, pictureUrl));
context.Identity.AddClaim(new Claim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl));
}
}

2
src/Squidex/Config/Identity/MyIdentityOptions.cs

@ -19,5 +19,7 @@ namespace Squidex.Config.Identity
public string GoogleSecret { get; set; }
public bool RequiresHttps { get; set; }
public bool LockAutomatically { get; set; }
}
}

3
src/Squidex/Controllers/Api/Apps/AppClientsController.cs

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Controllers.Api.Apps.Models;
using Squidex.Core.Identity;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
@ -24,7 +25,7 @@ namespace Squidex.Controllers.Api.Apps
/// <summary>
/// Manages and configures apps.
/// </summary>
[Authorize(Roles = "app-owner")]
[Authorize(Roles = SquidexRoles.AppOwner)]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
[SwaggerTag("Apps")]

3
src/Squidex/Controllers/Api/Apps/AppContributorsController.cs

@ -14,6 +14,7 @@ using NSwag.Annotations;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Controllers.Api.Apps.Models;
using Squidex.Core.Identity;
using Squidex.Pipeline;
using Squidex.Read.Apps.Services;
using Squidex.Write.Apps.Commands;
@ -23,7 +24,7 @@ namespace Squidex.Controllers.Api.Apps
/// <summary>
/// Manages and configures apps.
/// </summary>
[Authorize(Roles = "app-owner")]
[Authorize(Roles = SquidexRoles.AppOwner)]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
[SwaggerTag("Apps")]

3
src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs

@ -14,6 +14,7 @@ using NSwag.Annotations;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Controllers.Api.Apps.Models;
using Squidex.Core.Identity;
using Squidex.Infrastructure;
using Squidex.Pipeline;
using Squidex.Read.Apps.Services;
@ -24,7 +25,7 @@ namespace Squidex.Controllers.Api.Apps
/// <summary>
/// Manages and configures apps.
/// </summary>
[Authorize(Roles = "app-owner")]
[Authorize(Roles = SquidexRoles.AppOwner)]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
[SwaggerTag("Apps")]

3
src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs

@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Controllers.Api.Schemas.Models;
using Squidex.Core.Identity;
using Squidex.Pipeline;
using Squidex.Write.Schemas.Commands;
@ -20,7 +21,7 @@ namespace Squidex.Controllers.Api.Schemas
/// <summary>
/// Manages and retrieves information about schemas.
/// </summary>
[Authorize(Roles = "app-owner,app-developer")]
[Authorize(Roles = SquidexRoles.AppDeveloper)]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
[SwaggerTag("Schemas")]

3
src/Squidex/Controllers/Api/Schemas/SchemasController.cs

@ -15,6 +15,7 @@ using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Controllers.Api.Schemas.Models;
using Squidex.Controllers.Api.Schemas.Models.Converters;
using Squidex.Core.Identity;
using Squidex.Core.Schemas;
using Squidex.Pipeline;
using Squidex.Read.Schemas.Repositories;
@ -25,7 +26,7 @@ namespace Squidex.Controllers.Api.Schemas
/// <summary>
/// Manages and retrieves information about schemas.
/// </summary>
[Authorize(Roles = "app-owner,app-developer")]
[Authorize(Roles = SquidexRoles.AppDeveloper)]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
[SwaggerTag("Schemas")]

3
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Mvc;
using Squidex.Controllers.Api;
using Squidex.Controllers.ContentApi.Models;
using Squidex.Core.Contents;
using Squidex.Core.Identity;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
@ -25,7 +26,7 @@ using Squidex.Write.Contents.Commands;
namespace Squidex.Controllers.ContentApi
{
[Authorize(Roles = "app-editor,app-owner,app-developer")]
[Authorize(Roles = SquidexRoles.AppEditor)]
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
public class ContentsController : ControllerBase

3
src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs

@ -19,6 +19,7 @@ using NSwag.AspNetCore;
using NSwag.SwaggerGeneration;
using Squidex.Config;
using Squidex.Controllers.Api;
using Squidex.Core.Identity;
using Squidex.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;
@ -144,7 +145,7 @@ When you change the field to be localizable the value will become the value for
{
new SwaggerSecurityRequirement
{
{ "roles", new List<string> { "app-owner", "app-developer", "app-editor" } }
{ "roles", new List<string> { SquidexRoles.AppOwner, SquidexRoles.AppDeveloper, SquidexRoles.AppEditor } }
}
};

93
src/Squidex/Controllers/UI/Account/AccountController.cs

@ -6,19 +6,25 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.MongoDB;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NSwag.Annotations;
using Squidex.Infrastructure.Security;
using Microsoft.Extensions.Options;
using Squidex.Config;
using Squidex.Config.Identity;
using Squidex.Core.Identity;
using Squidex.Read.Users.Repositories;
// ReSharper disable InvertIf
// ReSharper disable RedundantIfElseBlock
// ReSharper disable ConvertIfStatementToReturnStatement
@ -27,21 +33,31 @@ namespace Squidex.Controllers.UI.Account
[SwaggerIgnore]
public sealed class AccountController : Controller
{
private static readonly EventId IdentityEventId = new EventId(8000, "IdentityEventId");
private readonly SignInManager<IdentityUser> signInManager;
private readonly UserManager<IdentityUser> userManager;
private readonly IOptions<MyIdentityOptions> identityOptions;
private readonly IOptions<MyUrlsOptions> urlOptions;
private readonly IUserRepository userRepository;
private readonly ILogger<AccountController> logger;
private readonly IIdentityServerInteractionService interactions;
public AccountController(
SignInManager<IdentityUser> signInManager,
UserManager<IdentityUser> userManager,
IOptions<MyIdentityOptions> identityOptions,
IOptions<MyUrlsOptions> urlOptions,
IUserRepository userRepository,
ILogger<AccountController> logger,
IIdentityServerInteractionService interactions)
{
this.signInManager = signInManager;
this.logger = logger;
this.urlOptions = urlOptions;
this.userManager = userManager;
this.userRepository = userRepository;
this.interactions = interactions;
this.identityOptions = identityOptions;
this.signInManager = signInManager;
}
[Authorize]
@ -125,15 +141,26 @@ namespace Squidex.Controllers.UI.Account
return RedirectToAction(nameof(Login));
}
var isLoggedIn = await LoginAsync(externalLogin);
var result = await signInManager.ExternalLoginSignInAsync(externalLogin.LoginProvider, externalLogin.ProviderKey, true);
if (!result.Succeeded && result.IsLockedOut)
{
return View("LockedOut");
}
var isLoggedIn = result.Succeeded;
if (!isLoggedIn)
{
var user = CreateUser(externalLogin);
var isFirst = await userRepository.CountAsync() == 0;
isLoggedIn =
await AddUserAsync(user) &&
await AddLoginAsync(user, externalLogin) &&
await MakeAdminAsync(user, isFirst) &&
await LockAsync(user, isFirst) &&
await LoginAsync(externalLogin);
}
@ -158,11 +185,9 @@ namespace Squidex.Controllers.UI.Account
return result.Succeeded;
}
private async Task<bool> AddUserAsync(IdentityUser user)
private Task<bool> AddUserAsync(IdentityUser user)
{
var result = await userManager.CreateAsync(user);
return result.Succeeded;
return MakeIdentityOperation("LoginAsync", () => userManager.CreateAsync(user));
}
private async Task<bool> LoginAsync(UserLoginInfo externalLogin)
@ -172,19 +197,39 @@ namespace Squidex.Controllers.UI.Account
return result.Succeeded;
}
private Task<bool> LockAsync(IdentityUser user, bool isFirst)
{
if (isFirst || !identityOptions.Value.LockAutomatically)
{
return Task.FromResult(false);
}
return MakeIdentityOperation("LockAsync", () => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100)));
}
private Task<bool> MakeAdminAsync(IdentityUser user, bool isFirst)
{
if (isFirst)
{
return Task.FromResult(false);
}
return MakeIdentityOperation("LockAsync", () => userManager.AddToRoleAsync(user, SquidexRoles.Administrator));
}
private static IdentityUser CreateUser(ExternalLoginInfo externalLogin)
{
var mail = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value;
var user = new IdentityUser { Email = mail, UserName = mail };
var pictureUrl = externalLogin.Principal.Claims.FirstOrDefault(x => x.Type == ExtendedClaimTypes.SquidexPictureUrl);
var pictureUrl = externalLogin.Principal.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.SquidexPictureUrl);
if (pictureUrl != null)
{
user.AddClaim(pictureUrl);
}
var displayName = externalLogin.Principal.Claims.FirstOrDefault(x => x.Type == ExtendedClaimTypes.SquidexDisplayName);
var displayName = externalLogin.Principal.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.SquidexDisplayName);
if (displayName != null)
{
user.AddClaim(displayName);
@ -192,5 +237,35 @@ namespace Squidex.Controllers.UI.Account
return user;
}
private async Task<bool> MakeIdentityOperation(string operationName, Func<Task<IdentityResult>> action)
{
try
{
var result = await action();
if (!result.Succeeded)
{
var errorMessageBuilder = new StringBuilder();
foreach (var error in result.Errors)
{
errorMessageBuilder.Append(error.Code);
errorMessageBuilder.Append(": ");
errorMessageBuilder.AppendLine(error.Description);
}
logger.LogError(IdentityEventId, "Operation '{0}' failed with errors: {1}", operationName, errorMessageBuilder.ToString());
}
return result.Succeeded;
}
catch (Exception e)
{
logger.LogError(IdentityEventId, e, "Operation '{0}' failed with exception", operationName);
return false;
}
}
}
}

23
src/Squidex/Pipeline/AppFilterAttribute.cs

@ -13,10 +13,13 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Core.Apps;
using Squidex.Core.Identity;
using Squidex.Infrastructure.Security;
using Squidex.Read.Apps;
using Squidex.Read.Apps.Services;
// ReSharper disable SwitchStatementMissingSomeCases
namespace Squidex.Pipeline
{
public sealed class AppFilterAttribute : Attribute, IAsyncAuthorizationFilter
@ -54,13 +57,23 @@ namespace Squidex.Pipeline
return;
}
var roleName = $"app-{permission.ToString().ToLowerInvariant()}";
var defaultIdentity = context.HttpContext.User.Identities.First();
defaultIdentity
.AddClaim(
new Claim(defaultIdentity.RoleClaimType, roleName));
switch (permission.Value)
{
case PermissionLevel.Owner:
defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppOwner));
defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppDeveloper));
defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppEditor));
break;
case PermissionLevel.Editor:
defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppDeveloper));
defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppEditor));
break;
case PermissionLevel.Developer:
defaultIdentity.AddClaim(new Claim(defaultIdentity.RoleClaimType, SquidexRoles.AppEditor));
break;
}
context.HttpContext.Features.Set<IAppFeature>(new AppFeature(app));
}

22
src/Squidex/Views/Account/LockedOut.cshtml

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title>Squidex - Account locked</title>
<style>
body {
padding: 40px;
}
h1, p {
text-align: center;
}
</style>
</head>
<body>
<h1>Account locked</h1>
<p>
Your account is locked, please contact the administrator.
</p>
</body>
</html>

1551
src/Squidex/app/theme/icomoon/selection.json

File diff suppressed because it is too large
Loading…
Cancel
Save