Browse Source

Refactoring for permission set.

pull/332/head
Sebastian Stehle 7 years ago
parent
commit
da27c542f1
  1. 12
      src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs
  2. 1
      src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  3. 31
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  4. 15
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs
  5. 6
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs
  6. 2
      src/Squidex.Domain.Apps.Entities/IAppProvider.cs
  7. 7
      src/Squidex.Domain.Users.MongoDb/MongoUser.cs
  8. 14
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  9. 4
      src/Squidex.Infrastructure/Security/Permission.cs
  10. 38
      src/Squidex.Infrastructure/Security/PermissionSet.cs
  11. 4
      src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs
  12. 12
      src/Squidex.Shared/Identity/SquidexClaimTypes.cs
  13. 27
      src/Squidex.Shared/Permissions.cs
  14. 2
      src/Squidex.Shared/Users/IUser.cs
  15. 70
      src/Squidex.Shared/Users/UserExtensions.cs
  16. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs
  17. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs
  18. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs
  19. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs
  20. 10
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  21. 13
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  22. 2
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  23. 2
      src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs
  24. 2
      src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs
  25. 2
      src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  26. 2
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  27. 2
      src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs
  28. 2
      src/Squidex/Areas/Api/Controllers/History/HistoryController.cs
  29. 2
      src/Squidex/Areas/Api/Controllers/Ping/PingController.cs
  30. 2
      src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs
  31. 6
      src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs
  32. 2
      src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs
  33. 2
      src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs
  34. 2
      src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs
  35. 6
      src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs
  36. 6
      src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs
  37. 6
      src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs
  38. 8
      src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs
  39. 6
      src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
  40. 5
      src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs
  41. 9
      src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs
  42. 5
      src/Squidex/Config/Web/WebServices.cs
  43. 15
      src/Squidex/Pipeline/ApiPermissionAttribute.cs
  44. 33
      src/Squidex/Pipeline/ApiPermissionUnifier.cs
  45. 29
      src/Squidex/Pipeline/AppResolver.cs
  46. 1
      src/Squidex/Pipeline/Swagger/SwaggerHelper.cs
  47. 4
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  48. 2
      src/Squidex/app/features/administration/pages/restore/restore-page.component.html
  49. 12
      src/Squidex/app/features/administration/pages/users/user-page.component.html
  50. 4
      src/Squidex/app/features/administration/pages/users/user-page.component.scss
  51. 11
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  52. 6
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  53. 13
      src/Squidex/app/features/administration/pages/users/users-page.component.ts
  54. 22
      src/Squidex/app/features/administration/services/users.service.spec.ts
  55. 6
      src/Squidex/app/features/administration/services/users.service.ts
  56. 14
      src/Squidex/app/features/administration/state/users.state.spec.ts
  57. 17
      src/Squidex/app/features/administration/state/users.state.ts
  58. 38
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  59. 14
      src/Squidex/app/shared/interceptors/auth.interceptor.ts
  60. 2
      src/Squidex/tsconfig.json
  61. 2
      tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  62. 11
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs
  63. 38
      tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs
  64. 2
      tests/Squidex.Tests/Pipeline/ApiCostsFilterTests.cs
  65. 2
      tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs
  66. 2
      tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs

12
src/Squidex.Domain.Apps.Core.Model/Apps/RoleExtension.cs

@ -7,11 +7,23 @@
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using System.Linq;
namespace Squidex.Domain.Apps.Core.Apps
{
public static class RoleExtension
{
public static string[] ToPermissionIds(this AppClientPermission clientPermission, string app)
{
return clientPermission.ToPermissions(app).ToIds().ToArray();
}
public static string[] ToPermissionIds(this AppContributorPermission contributorPermission, string app)
{
return contributorPermission.ToPermissions(app).ToIds().ToArray();
}
public static PermissionSet ToPermissions(this AppClientPermission clientPermission, string app)
{
Guard.Enum(clientPermission, nameof(clientPermission));

1
src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -19,6 +19,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>

31
src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -20,6 +20,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities
{
@ -158,23 +159,45 @@ namespace Squidex.Domain.Apps.Entities
});
}
public Task<List<IAppEntity>> GetUserApps(string userId)
public Task<List<IAppEntity>> GetUserApps(string userId, string[] permissions)
{
Guard.NotNull(userId, nameof(userId));
return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () =>
{
using (Profiler.TraceMethod<AppProvider>())
{
var ids = await grainFactory.GetGrain<IAppsByUserIndex>(userId).GetAppIdsAsync();
var ids =
await Task.WhenAll(
GetAppIdsByUserAsync(userId),
GetAppIdsAsync(permissions.ToAppNames()));
var apps =
await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync()));
await Task.WhenAll(ids
.SelectMany(x => x)
.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync()));
return apps.Where(a => IsFound(a.Value)).Select(a => a.Value).ToList();
}
});
}
private async Task<List<Guid>> GetAppIdsByUserAsync(string userId)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByUserIndex>(userId).GetAppIdsAsync();
}
}
private async Task<List<Guid>> GetAppIdsAsync(IEnumerable<string> names)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id).GetAppIdsAsync(names.ToArray());
}
}
private async Task<Guid> GetAppIdAsync(string name)
{
using (Profiler.TraceMethod<AppProvider>())

15
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs

@ -104,6 +104,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
return persistence.WriteSnapshotAsync(state);
}
public Task<List<Guid>> GetAppIdsAsync(params string[] names)
{
var appIds = new List<Guid>();
foreach (var appName in names)
{
if (state.Apps.TryGetValue(appName, out var appId))
{
appIds.Add(appId);
}
}
return Task.FromResult(appIds);
}
public Task<Guid> GetAppIdAsync(string appName)
{
state.Apps.TryGetValue(appName, out var appId);

6
src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs

@ -24,8 +24,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
Task RemoveReservationAsync(Guid appId, string name);
Task<Guid> GetAppIdAsync(string name);
Task<List<Guid>> GetAppIdsAsync();
Task<List<Guid>> GetAppIdsAsync(string[] names);
Task<Guid> GetAppIdAsync(string name);
}
}

2
src/Squidex.Domain.Apps.Entities/IAppProvider.cs

@ -28,6 +28,6 @@ namespace Squidex.Domain.Apps.Entities
Task<List<IRuleEntity>> GetRulesAsync(Guid appId);
Task<List<IAppEntity>> GetUserApps(string userId);
Task<List<IAppEntity>> GetUserApps(string userId, string[] permissions);
}
}

7
src/Squidex.Domain.Users.MongoDb/MongoUser.cs

@ -136,6 +136,11 @@ namespace Squidex.Domain.Users.MongoDb
Logins.RemoveAll(l => l.LoginProvider == loginProvider && l.ProviderKey == providerKey);
}
public void RemoveClaims(string type)
{
Claims.RemoveAll(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
}
public void AddClaim(Claim claim)
{
Claims.Add(claim);
@ -173,7 +178,7 @@ namespace Squidex.Domain.Users.MongoDb
public void SetClaim(string type, string value)
{
Claims.RemoveAll(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
RemoveClaims(type);
AddClaim(new Claim(type, value));
}

14
src/Squidex.Domain.Users/UserManagerExtensions.cs

@ -45,7 +45,7 @@ namespace Squidex.Domain.Users
return result;
}
public static async Task<IUser> CreateAsync(this UserManager<IUser> userManager, IUserFactory factory, string email, string displayName, string password)
public static async Task<IUser> CreateAsync(this UserManager<IUser> userManager, IUserFactory factory, string email, string displayName, string password, string[] permissions = null)
{
var user = factory.Create(email);
@ -54,6 +54,11 @@ namespace Squidex.Domain.Users
user.SetDisplayName(displayName);
user.SetPictureUrlFromGravatar(email);
if (permissions != null)
{
user.SetPermissions(permissions);
}
await DoChecked(() => userManager.CreateAsync(user), "Cannot create user.");
if (!string.IsNullOrWhiteSpace(password))
@ -80,7 +85,7 @@ namespace Squidex.Domain.Users
return userManager.UpdateAsync(user);
}
public static async Task UpdateAsync(this UserManager<IUser> userManager, string id, string email, string displayName, string password)
public static async Task UpdateAsync(this UserManager<IUser> userManager, string id, string email, string displayName, string password, string[] permissions = null)
{
var user = await userManager.FindByIdAsync(id);
@ -100,6 +105,11 @@ namespace Squidex.Domain.Users
user.SetDisplayName(displayName);
}
if (permissions != null)
{
user.SetPermissions(permissions);
}
await DoChecked(() => userManager.UpdateAsync(user), "Cannot update user.");
if (!string.IsNullOrWhiteSpace(password))

4
src/Squidex.Infrastructure/Security/Permission.cs

@ -47,7 +47,9 @@ namespace Squidex.Infrastructure.Security
return null;
}
return new HashSet<string>(x.Split(AlternativeSeparators, StringSplitOptions.RemoveEmptyEntries), StringComparer.OrdinalIgnoreCase);
var alternatives = x.Split(AlternativeSeparators, StringSplitOptions.RemoveEmptyEntries);
return new HashSet<string>(alternatives, StringComparer.OrdinalIgnoreCase);
})
.ToArray();
}

38
src/Squidex.Infrastructure/Security/PermissionSet.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
@ -16,42 +17,55 @@ namespace Squidex.Infrastructure.Security
public static readonly PermissionSet Empty = new PermissionSet();
private readonly List<Permission> permissions;
private readonly Lazy<string> display;
public int Count
{
get { return permissions.Count; }
}
public PermissionSet(IEnumerable<Permission> permissions)
public PermissionSet(params Permission[] permissions)
: this((IEnumerable<Permission>)permissions)
{
Guard.NotNull(permissions, nameof(permissions));
this.permissions = permissions.ToList();
}
public PermissionSet(params Permission[] permissions)
public PermissionSet(IEnumerable<Permission> permissions)
{
Guard.NotNull(permissions, nameof(permissions));
this.permissions = permissions.ToList();
display = new Lazy<string>(() => string.Join(";", this.permissions));
}
public bool GivesPermissionTo(Permission other)
public bool Allows(Permission other)
{
if (other == null)
{
return false;
}
foreach (var permission in permissions)
return permissions.Any(x => x.Allows(other));
}
public bool Includes(Permission other)
{
if (other == null)
{
if (permission.Allows(other))
{
return true;
}
return false;
}
return false;
return permissions.Any(x => x.Includes(other));
}
public override string ToString()
{
return display.Value;
}
public IEnumerable<string> ToIds()
{
return permissions.Select(x => x.Id);
}
public IEnumerator<Permission> GetEnumerator()

4
src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs

@ -16,12 +16,12 @@ namespace Squidex.Shared.Identity
{
public static void SetDisplayName(this ClaimsIdentity identity, string displayName)
{
identity.AddClaim(new Claim(SquidexClaimTypes.SquidexDisplayName, displayName));
identity.AddClaim(new Claim(SquidexClaimTypes.DisplayName, displayName));
}
public static void SetPictureUrl(this ClaimsIdentity identity, string pictureUrl)
{
identity.AddClaim(new Claim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl));
identity.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, pictureUrl));
}
public static IEnumerable<Claim> GetSquidexClaims(this ClaimsPrincipal principal)

12
src/Squidex.Shared/Identity/SquidexClaimTypes.cs

@ -9,17 +9,17 @@ namespace Squidex.Shared.Identity
{
public static class SquidexClaimTypes
{
public static readonly string SquidexDisplayName = "urn:squidex:name";
public static readonly string DisplayName = "urn:squidex:name";
public static readonly string SquidexPictureUrl = "urn:squidex:picture";
public static readonly string PictureUrl = "urn:squidex:picture";
public static readonly string SquidexConsent = "urn:squidex:consent";
public static readonly string Consent = "urn:squidex:consent";
public static readonly string SquidexConsentForEmails = "urn:squidex:consent:emails";
public static readonly string ConsentForEmails = "urn:squidex:consent:emails";
public static readonly string SquidexHidden = "urn:squidex:hidden";
public static readonly string Hidden = "urn:squidex:hidden";
public static readonly string SquidexPermissions = "urn:squidex:permissions";
public static readonly string Permissions = "urn:squidex:permissions";
public static readonly string Prefix = "urn:squidex:";
}

27
src/Squidex.Domain.Apps.Core.Model/Permissions.cs → src/Squidex.Shared/Permissions.cs

@ -5,12 +5,15 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Core
namespace Squidex.Shared
{
public sealed class Permissions
public static class Permissions
{
public const string All = "squidex.*";
@ -113,5 +116,25 @@ namespace Squidex.Domain.Apps.Core
return new Permission(id.Replace("{app}", app ?? "*").Replace("{name}", schema ?? "*"));
}
public static string[] ToAppPermissionIds(this IEnumerable<string> permissions, string app)
{
var result = permissions.Where(x => x.StartsWith($"squidex.apps.{app}", StringComparison.OrdinalIgnoreCase)).ToArray();
return result;
}
public static string[] ToAppNames(this IEnumerable<string> permissions)
{
var result =
permissions
.Where(x => x.StartsWith("squidex.apps.", StringComparison.OrdinalIgnoreCase))
.Select(x => x.Split('.'))
.Select(x => x[2])
.Distinct()
.ToArray();
return result;
}
}
}

2
src/Squidex.Shared/Users/IUser.cs

@ -24,6 +24,8 @@ namespace Squidex.Shared.Users
IReadOnlyList<ExternalLogin> Logins { get; }
void RemoveClaims(string type);
void SetEmail(string email);
void SetClaim(string type, string value);

70
src/Squidex.Shared/Users/UserExtensions.cs

@ -7,6 +7,7 @@
using System;
using System.Linq;
using System.Security.Claims;
using Squidex.Infrastructure;
using Squidex.Shared.Identity;
@ -16,97 +17,122 @@ namespace Squidex.Shared.Users
{
public static void SetDisplayName(this IUser user, string displayName)
{
user.SetClaim(SquidexClaimTypes.SquidexDisplayName, displayName);
user.SetClaim(SquidexClaimTypes.DisplayName, displayName);
}
public static void SetPictureUrl(this IUser user, string pictureUrl)
{
user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, pictureUrl);
user.SetClaim(SquidexClaimTypes.PictureUrl, pictureUrl);
}
public static void SetPictureUrlToStore(this IUser user)
{
user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, "store");
user.SetClaim(SquidexClaimTypes.PictureUrl, "store");
}
public static void SetPictureUrlFromGravatar(this IUser user, string email)
{
user.SetClaim(SquidexClaimTypes.SquidexPictureUrl, GravatarHelper.CreatePictureUrl(email));
user.SetClaim(SquidexClaimTypes.PictureUrl, GravatarHelper.CreatePictureUrl(email));
}
public static void SetHidden(this IUser user, bool value)
{
user.SetClaim(SquidexClaimTypes.SquidexHidden, value.ToString());
user.SetClaim(SquidexClaimTypes.Hidden, value.ToString());
}
public static void SetConsent(this IUser user)
{
user.SetClaim(SquidexClaimTypes.SquidexConsent, "true");
user.SetClaim(SquidexClaimTypes.Consent, "true");
}
public static void SetConsentForEmails(this IUser user, bool value)
{
user.SetClaim(SquidexClaimTypes.SquidexConsentForEmails, value.ToString());
user.SetClaim(SquidexClaimTypes.ConsentForEmails, value.ToString());
}
public static void SetPermissions(this IUser user, params string[] permissions)
{
user.RemoveClaims(SquidexClaimTypes.Permissions);
foreach (var permission in permissions)
{
user.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission));
}
}
public static bool IsHidden(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexHidden, "true");
return user.HasClaimValue(SquidexClaimTypes.Hidden, "true");
}
public static bool HasConsent(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexConsent, "true");
return user.HasClaimValue(SquidexClaimTypes.Consent, "true");
}
public static bool HasConsentForEmails(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexConsentForEmails, "true");
return user.HasClaimValue(SquidexClaimTypes.ConsentForEmails, "true");
}
public static bool HasDisplayName(this IUser user)
{
return user.HasClaim(SquidexClaimTypes.SquidexDisplayName);
return user.HasClaim(SquidexClaimTypes.DisplayName);
}
public static bool HasPictureUrl(this IUser user)
{
return user.HasClaim(SquidexClaimTypes.SquidexPictureUrl);
return user.HasClaim(SquidexClaimTypes.PictureUrl);
}
public static bool IsPictureUrlStored(this IUser user)
{
return user.HasClaimValue(SquidexClaimTypes.SquidexPictureUrl, "store");
return user.HasClaimValue(SquidexClaimTypes.PictureUrl, "store");
}
public static string PictureUrl(this IUser user)
{
return user.GetClaimValue(SquidexClaimTypes.SquidexPictureUrl);
return user.GetClaimValue(SquidexClaimTypes.PictureUrl);
}
public static string DisplayName(this IUser user)
{
return user.GetClaimValue(SquidexClaimTypes.SquidexDisplayName);
return user.GetClaimValue(SquidexClaimTypes.DisplayName);
}
public static string[] Permissions(this ClaimsPrincipal principal)
{
return principal.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).Select(x => x.Value).ToArray();
}
public static string[] Permissions(this IUser user)
{
return user.GetClaimValues(SquidexClaimTypes.Permissions);
}
public static string GetClaimValue(this IUser user, string type)
{
return user.Claims.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase))?.Value;
}
public static string GetClaimValue(this IUser user, string claim)
public static string[] GetClaimValues(this IUser user, string type)
{
return user.Claims.FirstOrDefault(x => string.Equals(x.Type, claim, StringComparison.OrdinalIgnoreCase))?.Value;
return user.Claims.Where(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)).Select(x => x.Value).ToArray();
}
public static bool HasClaim(this IUser user, string claim)
public static bool HasClaim(this IUser user, string type)
{
return user.Claims.Any(x => string.Equals(x.Type, claim, StringComparison.OrdinalIgnoreCase));
return user.Claims.Any(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
}
public static bool HasClaimValue(this IUser user, string claim, string value)
public static bool HasClaimValue(this IUser user, string type, string value)
{
return user.Claims.Any(x => string.Equals(x.Type, claim, StringComparison.OrdinalIgnoreCase) && string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase));
return user.Claims.Any(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase) && string.Equals(x.Value, value, StringComparison.OrdinalIgnoreCase));
}
public static string PictureNormalizedUrl(this IUser user)
{
var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.SquidexPictureUrl)?.Value;
var url = user.Claims.FirstOrDefault(x => x.Type == SquidexClaimTypes.PictureUrl)?.Value;
if (!string.IsNullOrWhiteSpace(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute) && url.Contains("gravatar"))
{

2
src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs

@ -9,10 +9,10 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps
{

2
src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs

@ -8,11 +8,11 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps
{

2
src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs

@ -9,11 +9,11 @@ using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps
{

2
src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs

@ -10,10 +10,10 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps
{

10
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -10,13 +10,14 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;
using Squidex.Pipeline;
using Squidex.Shared;
using Squidex.Shared.Users;
namespace Squidex.Areas.Api.Controllers.Apps
{
@ -55,11 +56,12 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)]
public async Task<IActionResult> GetApps()
{
var subject = HttpContext.User.OpenIdSubject();
var userId = HttpContext.User.OpenIdSubject();
var userPermissions = HttpContext.User.Permissions();
var entities = await appProvider.GetUserApps(subject);
var entities = await appProvider.GetUserApps(userId, userPermissions);
var response = entities.Select(a => AppDto.FromApp(a, subject, appPlansProvider)).ToList();
var response = entities.Select(a => AppDto.FromApp(a, userId, userPermissions, appPlansProvider)).ToList();
Response.Headers["ETag"] = response.ToManyEtag();

13
src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs

@ -7,7 +7,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Newtonsoft.Json;
using NodaTime;
using Squidex.Domain.Apps.Core.Apps;
@ -15,6 +14,7 @@ using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -62,11 +62,18 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public string PlanUpgrade { get; set; }
public static AppDto FromApp(IAppEntity app, string subject, IAppPlansProvider plans)
public static AppDto FromApp(IAppEntity app, string userId, string[] permissions, IAppPlansProvider plans)
{
var response = SimpleMapper.Map(app, new AppDto());
response.Permissions = app.Contributors[subject].ToPermissions(app.Name).Select(x => x.Id).ToArray();
if (app.Contributors.TryGetValue(userId, out var role))
{
response.Permissions = role.ToPermissionIds(app.Name);
}
else
{
response.Permissions = permissions.ToAppPermissionIds(app.Name);
}
response.PlanName = plans.GetPlanForApp(app)?.Name;
response.PlanUpgrade = plans.GetPlanUpgradeForApp(app)?.Name;

2
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -14,7 +14,6 @@ using Microsoft.Extensions.Options;
using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Areas.Api.Controllers.Contents;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Services;
@ -25,6 +24,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Assets
{

2
src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs

@ -12,11 +12,11 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Orleans;
using Squidex.Areas.Api.Controllers.Backups.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Tasks;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Backups
{

2
src/Squidex/Areas/Api/Controllers/Backups/RestoreController.cs

@ -9,11 +9,11 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Orleans;
using Squidex.Areas.Api.Controllers.Backups.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Backups
{

2
src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs

@ -10,12 +10,12 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Orleans;
using Squidex.Areas.Api.Controllers.Comments.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Comments
{

2
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -13,7 +13,6 @@ using Microsoft.Extensions.Options;
using NodaTime;
using NodaTime.Text;
using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents;
@ -21,6 +20,7 @@ using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Contents
{

2
src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs

@ -10,11 +10,11 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Orleans;
using Squidex.Areas.Api.Controllers.EventConsumers.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Orleans;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.EventConsumers
{

2
src/Squidex/Areas/Api/Controllers/History/HistoryController.cs

@ -9,10 +9,10 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.History.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.History.Repositories;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.History
{

2
src/Squidex/Areas/Api/Controllers/Ping/PingController.cs

@ -6,9 +6,9 @@
// ==========================================================================
using Microsoft.AspNetCore.Mvc;
using Squidex.Domain.Apps.Core;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Ping
{

2
src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs

@ -8,10 +8,10 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Plans.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Plans
{

6
src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs

@ -13,7 +13,6 @@ using IdentityServer4.Models;
using Microsoft.AspNetCore.Mvc;
using NodaTime;
using Squidex.Areas.Api.Controllers.Rules.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
@ -21,6 +20,7 @@ using Squidex.Extensions.Actions;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Rules
{
@ -53,7 +53,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
[HttpGet]
[Route("rules/actions/")]
[ProducesResponseType(typeof(Dictionary<string, RuleElementDto>), 200)]
[ApiPermission(Permissions.AppRulesRead)]
[ApiPermission]
[ApiCosts(0)]
public IActionResult GetActions()
{
@ -73,7 +73,7 @@ namespace Squidex.Areas.Api.Controllers.Rules
[HttpGet]
[Route("rules/triggers/")]
[ProducesResponseType(typeof(Dictionary<string, RuleElementDto>), 200)]
[ApiPermission(Permissions.AppRulesRead)]
[ApiPermission]
[ApiCosts(0)]
public IActionResult GetTriggers()
{

2
src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs

@ -8,10 +8,10 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Schemas
{

2
src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs

@ -10,12 +10,12 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Schemas
{

2
src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs

@ -11,12 +11,12 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Statistics.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Statistics
{

6
src/Squidex/Areas/Api/Controllers/Users/Models/CreateUserDto.cs

@ -29,5 +29,11 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
/// </summary>
[Required]
public string Password { get; set; }
/// <summary>
/// Additional permissions for the user.
/// </summary>
[Required]
public string[] Permissions { get; set; }
}
}

6
src/Squidex/Areas/Api/Controllers/Users/Models/UpdateUserDto.cs

@ -28,5 +28,11 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
/// The password of the user.
/// </summary>
public string Password { get; set; }
/// <summary>
/// Additional permissions for the user.
/// </summary>
[Required]
public string[] Permissions { get; set; }
}
}

6
src/Squidex/Areas/Api/Controllers/Users/Models/UserCreatedDto.cs

@ -16,5 +16,11 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
/// </summary>
[Required]
public string Id { get; set; }
/// <summary>
/// Additional permissions for the user.
/// </summary>
[Required]
public string[] Permissions { get; set; }
}
}

8
src/Squidex/Areas/Api/Controllers/Users/Models/UserDto.cs

@ -37,9 +37,15 @@ namespace Squidex.Areas.Api.Controllers.Users.Models
[Required]
public bool IsLocked { get; set; }
/// <summary>
/// Additional permissions for the user.
/// </summary>
[Required]
public string[] Permissions { get; set; }
public static UserDto FromUser(IUser user)
{
return SimpleMapper.Map(user, new UserDto { DisplayName = user.DisplayName() });
return SimpleMapper.Map(user, new UserDto { DisplayName = user.DisplayName(), Permissions = user.Permissions() });
}
}
}

6
src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs

@ -11,12 +11,12 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Users.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;
using Squidex.Pipeline;
using Squidex.Shared;
using Squidex.Shared.Users;
namespace Squidex.Areas.Api.Controllers.Users
@ -75,7 +75,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersCreate)]
public async Task<IActionResult> PostUser([FromBody] CreateUserDto request)
{
var user = await userManager.CreateAsync(userFactory, request.Email, request.DisplayName, request.Password);
var user = await userManager.CreateAsync(userFactory, request.Email, request.DisplayName, request.Password, request.Permissions);
var response = new UserCreatedDto { Id = user.Id };
@ -87,7 +87,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersUpdate)]
public async Task<IActionResult> PutUser(string id, [FromBody] UpdateUserDto request)
{
await userManager.UpdateAsync(id, request.Email, request.DisplayName, request.Password);
await userManager.UpdateAsync(id, request.Email, request.DisplayName, request.Password, request.Permissions);
return NoContent();
}

5
src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs

@ -15,6 +15,7 @@ using Microsoft.Extensions.Options;
using Squidex.Config;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Log;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
@ -66,9 +67,7 @@ namespace Squidex.Areas.IdentityServer.Config
{
try
{
var user = await userManager.CreateAsync(userFactory, adminEmail, adminEmail, adminPass);
await userManager.AddToRoleAsync(user, SquidexRoles.Administrator);
await userManager.CreateAsync(userFactory, adminEmail, adminEmail, adminPass, new[] { Permissions.Admin });
}
catch (Exception ex)
{

9
src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs

@ -83,7 +83,8 @@ namespace Squidex.Areas.IdentityServer.Config
UserClaims = new List<string>
{
JwtClaimTypes.Email,
JwtClaimTypes.Role
JwtClaimTypes.Role,
SquidexClaimTypes.Permissions
}
};
}
@ -101,13 +102,13 @@ namespace Squidex.Areas.IdentityServer.Config
yield return new IdentityResource(Constants.PermissionsScope,
new[]
{
SquidexClaimTypes.SquidexPermissions
SquidexClaimTypes.Permissions
});
yield return new IdentityResource(Constants.ProfileScope,
new[]
{
SquidexClaimTypes.SquidexDisplayName,
SquidexClaimTypes.SquidexPictureUrl
SquidexClaimTypes.DisplayName,
SquidexClaimTypes.PictureUrl
});
}
}

5
src/Squidex/Config/Web/WebServices.cs

@ -18,7 +18,7 @@ namespace Squidex.Config.Web
services.AddSingletonAs<FileCallbackResultExecutor>()
.AsSelf();
services.AddSingletonAs<AppResolverFilter>()
services.AddSingletonAs<AppResolver>()
.AsSelf();
services.AddSingletonAs<ApiCostsFilter>()
@ -36,7 +36,8 @@ namespace Squidex.Config.Web
services.AddMvc(options =>
{
options.Filters.Add<ETagFilter>();
options.Filters.Add<AppResolverFilter>();
options.Filters.Add<ApiPermissionUnifier>();
options.Filters.Add<AppResolver>();
}).AddMySerializers();
services.AddCors();

15
src/Squidex/Pipeline/ApiPermissionAttribute.cs

@ -32,6 +32,11 @@ namespace Squidex.Pipeline
{
if (permissionIds.Length > 0)
{
var set = new PermissionSet(
context.HttpContext.User.FindAll(SquidexClaimTypes.Permissions)
.Select(x => x.Value)
.Select(x => new Permission(x)));
var hasPermission = false;
foreach (var permissionId in permissionIds)
@ -43,15 +48,7 @@ namespace Squidex.Pipeline
id = id.Replace($"{{{routeParam.Key}}}", routeParam.Value?.ToString());
}
var set = new PermissionSet(
context.HttpContext.User.FindAll(SquidexClaimTypes.SquidexPermissions)
.Select(x => x.Value)
.Select(x => new Permission(x)));
if (set.GivesPermissionTo(new Permission(id)))
{
hasPermission = true;
}
hasPermission |= set.Allows(new Permission(id));
}
if (!hasPermission)

33
src/Squidex/Pipeline/ApiPermissionUnifier.cs

@ -0,0 +1,33 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Shared;
using Squidex.Shared.Identity;
namespace Squidex.Pipeline
{
public sealed class ApiPermissionUnifier : IAsyncActionFilter
{
public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var user = context.HttpContext.User;
var identity = user.Identities.First();
if (string.Equals(identity.FindFirst(identity.RoleClaimType)?.Value, SquidexRoles.Administrator))
{
identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.Admin));
}
return next();
}
}
}

29
src/Squidex/Pipeline/AppResolverFilter.cs → src/Squidex/Pipeline/AppResolver.cs

@ -10,16 +10,17 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Pipeline
{
public sealed class AppResolverFilter : IAsyncActionFilter
public sealed class AppResolver : IAsyncActionFilter
{
private readonly IAppProvider appProvider;
@ -33,7 +34,7 @@ namespace Squidex.Pipeline
}
}
public AppResolverFilter(IAppProvider appProvider)
public AppResolver(IAppProvider appProvider)
{
this.appProvider = appProvider;
}
@ -42,13 +43,6 @@ namespace Squidex.Pipeline
{
var user = context.HttpContext.User;
var identity = user.Identities.First();
if (string.Equals(identity.FindFirst(identity.RoleClaimType)?.Value, SquidexRoles.Administrator))
{
identity.AddClaim(new Claim(SquidexClaimTypes.SquidexPermissions, Permissions.Admin));
}
var appName = context.RouteData.Values["app"]?.ToString();
if (!string.IsNullOrWhiteSpace(appName))
@ -67,13 +61,20 @@ namespace Squidex.Pipeline
if (permissions.Count == 0)
{
context.Result = new NotFoundResult();
return;
var set = new PermissionSet(user.Permissions().Select(x => new Permission(x)));
if (!set.Includes(Permissions.ForApp(Permissions.App, appName)))
{
context.Result = new NotFoundResult();
return;
}
}
var identity = user.Identities.First();
foreach (var permission in permissions)
{
identity.AddClaim(new Claim(SquidexClaimTypes.SquidexPermissions, permission.Id));
identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission.Id));
}
context.HttpContext.Features.Set<IAppFeature>(new AppFeature(app));
@ -96,7 +97,7 @@ namespace Squidex.Pipeline
private static PermissionSet FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user)
{
var subjectId = user.FindFirst(OpenIdClaims.Subject)?.Value;
var subjectId = user.OpenIdSubject();
if (subjectId != null && app.Contributors.TryGetValue(subjectId, out var permission))
{

1
src/Squidex/Pipeline/Swagger/SwaggerHelper.cs

@ -16,7 +16,6 @@ using NJsonSchema;
using NJsonSchema.Generation;
using NSwag;
using Squidex.Config;
using Squidex.Shared.Identity;
namespace Squidex.Pipeline.Swagger
{

4
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html

@ -23,7 +23,7 @@
<th class="cell-auto-right">
Position
</th>
<th class="cell-actions-lg" *sqxPermission="'squidex.admin.events.manage'">
<th class="cell-actions-lg">
Actions
</th>
</tr>
@ -41,7 +41,7 @@
<td class="cell-auto-right">
<span>{{eventConsumer.position}}</span>
</td>
<td class="cell-actions-lg" *sqxPermission="'squidex.admin.events.manage'">
<td class="cell-actions-lg">
<button class="btn btn-link" (click)="reset(eventConsumer)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer">
<i class="icon icon-reset"></i>
</button>

2
src/Squidex/app/features/administration/pages/restore/restore-page.component.html

@ -49,7 +49,7 @@
</div>
</ng-container>
<div class="table-items-row" *sqxPermission="'squidex.admin.restore.create'">
<div class="table-items-row">
<form [formGroup]="restoreForm.form" (submit)="restore()">
<div class="row no-gutters">
<div class="col">

12
src/Squidex/app/features/administration/pages/users/user-page.component.html

@ -16,7 +16,7 @@
<ng-container menu>
<ng-container *ngIf="usersState.selectedUser | async; else noUserMenu">
<ng-container *ngIf="canUpdate">
<ng-container>
<button type="submit" class="btn btn-primary" title="CTRL + S">
Save
</button>
@ -49,7 +49,7 @@
<sqx-control-errors for="displayName" [submitted]="userForm.submitted | async"></sqx-control-errors>
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="false" />
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="false" spellcheck="false" />
</div>
<div class="form-group form-group-password" [class.hidden]="user?.isCurrentUser">
@ -69,6 +69,14 @@
<input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" autocomplete="false" />
</div>
</div>
<div class="form-group">
<label for="permissions">Permissions</label>
<sqx-control-errors for="permissions" [submitted]="userForm.submitted | async"></sqx-control-errors>
<textarea class="form-control" id="permissions" formControlName="permissions" placeholder="Separate by line" autocomplete="false" spellcheck="false"></textarea>
</div>
</ng-container>
</sqx-panel>
</form>

4
src/Squidex/app/features/administration/pages/users/user-page.component.scss

@ -3,4 +3,8 @@
.form-group-password {
margin-top: 2rem;
}
textarea {
height: 200px;
}

11
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -10,13 +10,9 @@ import { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { AuthService, Permission, permissionsAllow } from '@app/shared';
import { UserDto } from './../../services/users.service';
import { UserForm, UsersState } from './../../state/users.state';
const UserUpdatePermission = new Permission('squidex.admin.users.update');
@Component({
selector: 'sqx-user-page',
styleUrls: ['./user-page.component.scss'],
@ -30,13 +26,12 @@ export class UserPageComponent implements OnDestroy, OnInit {
public user?: { user: UserDto, isCurrentUser: boolean };
public userForm = new UserForm(this.formBuilder);
constructor(authService: AuthService,
constructor(
public readonly usersState: UsersState,
private readonly formBuilder: FormBuilder,
private readonly route: ActivatedRoute,
private readonly router: Router
) {
this.canUpdate = permissionsAllow(authService.user!.permissions, UserUpdatePermission);
}
public ngOnDestroy() {
@ -52,10 +47,6 @@ export class UserPageComponent implements OnDestroy, OnInit {
if (selectedUser) {
this.userForm.load(selectedUser.user);
}
if (!this.canUpdate && selectedUser) {
this.userForm.form.disable();
}
});
}

6
src/Squidex/app/features/administration/pages/users/users-page.component.html

@ -18,7 +18,7 @@
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" />
</form>
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)" *sqxPermission="'squidex.admin.users.create'">
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)">
<i class="icon-plus"></i> New
</button>
</ng-container>
@ -37,7 +37,7 @@
<th class="cell-auto">
Email
</th>
<th class="cell-actions" *ngIf="canLock">
<th class="cell-actions">
Actions
</th>
</tr>
@ -59,7 +59,7 @@
<td class="cell-auto">
<span class="user-email table-cell">{{userInfo.user.email}}</span>
</td>
<td class="cell-actions" *ngIf="canLock">
<td class="cell-actions">
<ng-container *ngIf="!userInfo.isCurrentUser">
<button class="btn btn-link" (click)="lock(userInfo.user); $event.stopPropagation();" *ngIf="!userInfo.user.isLocked" title="Lock User">
<i class="icon icon-unlocked"></i>

13
src/Squidex/app/features/administration/pages/users/users-page.component.ts

@ -9,17 +9,9 @@ import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { onErrorResumeNext } from 'rxjs/operators';
import {
AuthService,
Permission,
permissionsAllow
} from '@app/shared';
import { UserDto } from './../../services/users.service';
import { UsersState } from './../../state/users.state';
const UserLockPermission = new Permission('squidex.admin.users.lock');
@Component({
selector: 'sqx-users-page',
styleUrls: ['./users-page.component.scss'],
@ -28,12 +20,9 @@ const UserLockPermission = new Permission('squidex.admin.users.lock');
export class UsersPageComponent implements OnInit {
public usersFilter = new FormControl();
public canLock: boolean;
constructor(authService: AuthService,
constructor(
public readonly usersState: UsersState
) {
this.canLock = permissionsAllow(authService.user!.permissions, UserLockPermission);
}
public ngOnInit() {

22
src/Squidex/app/features/administration/services/users.service.spec.ts

@ -56,12 +56,14 @@ describe('UsersService', () => {
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
permissions: ['Permission1'],
isLocked: true
},
{
id: '456',
email: 'mail2@domain.com',
displayName: 'User2',
permissions: ['Permission2'],
isLocked: true
}
]
@ -69,8 +71,8 @@ describe('UsersService', () => {
expect(users!).toEqual(
new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', true),
new UserDto('456', 'mail2@domain.com', 'User2', true)
new UserDto('123', 'mail1@domain.com', 'User1', ['Permission1'], true),
new UserDto('456', 'mail2@domain.com', 'User2', ['Permission2'], true)
]));
}));
@ -95,12 +97,14 @@ describe('UsersService', () => {
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
permissions: ['Permission1'],
isLocked: true
},
{
id: '456',
email: 'mail2@domain.com',
displayName: 'User2',
permissions: ['Permission2'],
isLocked: true
}
]
@ -108,8 +112,8 @@ describe('UsersService', () => {
expect(users!).toEqual(
new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', true),
new UserDto('456', 'mail2@domain.com', 'User2', true)
new UserDto('123', 'mail1@domain.com', 'User1', ['Permission1'], true),
new UserDto('456', 'mail2@domain.com', 'User2', ['Permission2'], true)
]));
}));
@ -131,17 +135,17 @@ describe('UsersService', () => {
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
permissions: ['Permission1'],
isLocked: true
});
expect(user!).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', true));
expect(user!).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', ['Permission1'], true));
}));
it('should make post request to create user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = new CreateUserDto('mail@squidex.io', 'Squidex User', 'password');
const dto = new CreateUserDto('mail@squidex.io', 'Squidex User', ['Permission1'], 'password');
let user: UserDto;
@ -156,13 +160,13 @@ describe('UsersService', () => {
req.flush({ id: '123', pictureUrl: 'path/to/image1' });
expect(user!).toEqual(new UserDto('123', dto.email, dto.displayName, false));
expect(user!).toEqual(new UserDto('123', dto.email, dto.displayName, dto.permissions, false));
}));
it('should make put request to update user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = new UpdateUserDto('mail@squidex.io', 'Squidex User', 'password');
const dto = new UpdateUserDto('mail@squidex.io', 'Squidex User', ['Permission1'], 'password');
userManagementService.putUser('123', dto).subscribe();

6
src/Squidex/app/features/administration/services/users.service.ts

@ -31,6 +31,7 @@ export class UserDto extends Model {
public readonly id: string,
public readonly email: string,
public readonly displayName: string,
public readonly permissions: string[],
public readonly isLocked: boolean
) {
super();
@ -45,6 +46,7 @@ export class CreateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly permissions: string[],
public readonly password: string
) {
}
@ -54,6 +56,7 @@ export class UpdateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly permissions: string[],
public readonly password?: string
) {
}
@ -81,6 +84,7 @@ export class UsersService {
item.id,
item.email,
item.displayName,
item.permissions,
item.isLocked);
});
@ -100,6 +104,7 @@ export class UsersService {
body.id,
body.email,
body.displayName,
body.permissions,
body.isLocked);
}),
pretifyError('Failed to load user. Please reload.'));
@ -116,6 +121,7 @@ export class UsersService {
body.id,
dto.email,
dto.displayName,
dto.permissions,
false);
}),
pretifyError('Failed to create user. Please reload.'));

14
src/Squidex/app/features/administration/state/users.state.spec.ts

@ -22,11 +22,11 @@ import {
describe('UsersState', () => {
const oldUsers = [
new UserDto('id1', 'mail1@mail.de', 'name1', false),
new UserDto('id2', 'mail2@mail.de', 'name2', true)
new UserDto('id1', 'mail1@mail.de', 'name1', ['Permission1'], false),
new UserDto('id2', 'mail2@mail.de', 'name2', ['Permission2'], true)
];
const newUser = new UserDto('id3', 'mail3@mail.de', 'name3', false);
const newUser = new UserDto('id3', 'mail3@mail.de', 'name3', ['Permission3'], false);
let authService: IMock<AuthService>;
let dialogs: IMock<DialogService>;
@ -73,8 +73,8 @@ describe('UsersState', () => {
usersState.select('id1').subscribe();
const newUsers = [
new UserDto('id1', 'mail1@mail.de_new', 'name1_new', false),
new UserDto('id2', 'mail2@mail.de_new', 'name2_new', true)
new UserDto('id1', 'mail1@mail.de_new', 'name1_new', ['Permission1_New'], false),
new UserDto('id2', 'mail2@mail.de_new', 'name2_new', ['Permission2_New'], true)
];
usersService.setup(x => x.getUsers(10, 0, undefined))
@ -168,7 +168,7 @@ describe('UsersState', () => {
});
it('should update user properties when updated', () => {
const request = new UpdateUserDto('new@mail.com', 'New');
const request = new UpdateUserDto('new@mail.com', 'New', ['Permission1']);
usersService.setup(x => x.putUser('id1', request))
.returns(() => of({}));
@ -184,7 +184,7 @@ describe('UsersState', () => {
});
it('should add user to snapshot when created', () => {
const request = new CreateUserDto(newUser.email, newUser.displayName, 'password');
const request = new CreateUserDto(newUser.email, newUser.displayName, newUser.permissions, 'password');
usersService.setup(x => x.postUser(request))
.returns(() => of(newUser));

17
src/Squidex/app/features/administration/state/users.state.ts

@ -57,18 +57,31 @@ export class UserForm extends Form<FormGroup> {
[
ValidatorsEx.match('password', 'Passwords must be the same.')
]
]
],
permissions: ['']
}));
}
public load(user?: UserDto) {
if (user) {
this.form.controls['password'].setValidators(null);
super.load({ ...user, permissions: user.permissions.join('\n') });
} else {
this.form.controls['password'].setValidators(Validators.required);
super.load(undefined);
}
}
public submit() {
const result = super.submit();
if (result) {
result['permissions'] = result['permissions'].split('\n').filter((x: any) => !!x);
}
super.load(user);
return result;
}
}

38
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -46,26 +46,28 @@
</tbody>
</table>
<div class="table-items-footer" *ngIf="(contributorsState.isMaxReached | async) === false">
<form [formGroup]="assignContributorForm.form" (ngSubmit)="assignContributor()">
<div class="row no-gutters">
<div class="col">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'" placeholder="Find existing user" displayProperty="displayName">
<ng-template let-user="$implicit">
<span class="autocomplete-user">
<img class="user-picture autocomplete-user-picture" [attr.src]="user | sqxUserDtoPicture" />
<ng-container>
<div class="table-items-footer" *ngIf="(contributorsState.isMaxReached | async) === false">
<form [formGroup]="assignContributorForm.form" (ngSubmit)="assignContributor()">
<div class="row no-gutters">
<div class="col">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'" placeholder="Find existing user" displayProperty="displayName">
<ng-template let-user="$implicit">
<span class="autocomplete-user">
<img class="user-picture autocomplete-user-picture" [attr.src]="user | sqxUserDtoPicture" />
<span class="user-name autocomplete-user-name">{{user.displayName}}</span>
</span>
</ng-template>
</sqx-autocomplete>
<span class="user-name autocomplete-user-name">{{user.displayName}}</span>
</span>
</ng-template>
</sqx-autocomplete>
</div>
<div class="col col-auto pl-1">
<button type="submit" class="btn btn-success" [disabled]="assignContributorForm.hasNoUser | async">Add Contributor</button>
</div>
</div>
<div class="col col-auto pl-1">
<button type="submit" class="btn btn-success" [disabled]="assignContributorForm.hasNoUser | async">Add Contributor</button>
</div>
</div>
</form>
</div>
</form>
</div>
</ng-container>
</ng-container>
</ng-container>
</ng-container>

14
src/Squidex/app/shared/interceptors/auth.interceptor.ts

@ -10,7 +10,7 @@ import { Injectable} from '@angular/core';
import { empty, Observable, throwError } from 'rxjs';
import { catchError, switchMap, take } from 'rxjs/operators';
import { ApiUrlConfig } from '@app/framework';
import { ApiUrlConfig, ErrorDto } from '@app/framework';
import { AuthService, Profile } from './../services/auth.service';
@ -39,14 +39,14 @@ export class AuthInterceptor implements HttpInterceptor {
private makeRequest(req: HttpRequest<any>, next: HttpHandler, user: Profile | null, renew = false): Observable<HttpEvent<any>> {
const token = user ? user.authToken : '';
const authReq = req.clone({
req = req.clone({
headers: req.headers
.set('Authorization', token)
.set('Accept-Language', '*')
.set('Pragma', 'no-cache')
});
return next.handle(authReq).pipe(
return next.handle(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401 && renew) {
return this.authService.loginSilent().pipe(
@ -57,9 +57,13 @@ export class AuthInterceptor implements HttpInterceptor {
}),
switchMap(u => this.makeRequest(req, next, u)));
} else if (error.status === 401 || error.status === 403) {
this.authService.logoutRedirect();
if (req.method === 'GET') {
this.authService.logoutRedirect();
return empty();
return empty();
} else {
return throwError(new ErrorDto(403, 'You do not have the permissions to do this.'));
}
}
return throwError(error);

2
src/Squidex/tsconfig.json

@ -17,7 +17,7 @@
"target": "es5",
"paths": {
"@app*": [
"app/*"
"app*"
]
}
},

2
tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs

@ -38,7 +38,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.Returns("me@email.com");
A.CallTo(() => user.Claims)
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.SquidexDisplayName, "me") });
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "me") });
sut = new RuleEventFormatter(serializer, urlGenerator);
}

11
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs

@ -105,6 +105,17 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
Assert.True(await sut.ReserveAppAsync(appId1, appName1));
}
[Fact]
public async Task Should_return_many_app_ids()
{
await sut.AddAppAsync(appId1, appName1);
await sut.AddAppAsync(appId2, appName2);
var ids = await sut.GetAppIdsAsync(appName1, appName2);
Assert.Equal(new List<Guid> { appId1, appId2 }, ids);
}
[Fact]
public async Task Should_remove_app_id_from_index()
{

38
tests/Squidex.Infrastructure.Tests/Security/PermissionSetTests.cs

@ -30,6 +30,8 @@ namespace Squidex.Infrastructure.Security
Assert.Equal(((IEnumerable)sut).OfType<Permission>().ToList(), source);
Assert.Equal(3, source.Count);
Assert.Equal("c;b;a", sut.ToString());
}
[Fact]
@ -39,7 +41,17 @@ namespace Squidex.Infrastructure.Security
new Permission("app.contents"),
new Permission("app.assets"));
Assert.True(sut.GivesPermissionTo(new Permission("app.contents")));
Assert.True(sut.Allows(new Permission("app.contents")));
}
[Fact]
public void Should_return_true_if_any_permission_includes_given()
{
var sut = new PermissionSet(
new Permission("app.contents"),
new Permission("app.assets"));
Assert.True(sut.Includes(new Permission("app")));
}
[Fact]
@ -49,7 +61,17 @@ namespace Squidex.Infrastructure.Security
new Permission("app.contents"),
new Permission("app.assets"));
Assert.False(sut.GivesPermissionTo(new Permission("app.schemas")));
Assert.False(sut.Allows(new Permission("app.schemas")));
}
[Fact]
public void Should_return_false_if_none_permission_includes_given()
{
var sut = new PermissionSet(
new Permission("app.contents"),
new Permission("app.assets"));
Assert.False(sut.Includes(new Permission("other")));
}
[Fact]
@ -59,7 +81,17 @@ namespace Squidex.Infrastructure.Security
new Permission("app.contents"),
new Permission("app.assets"));
Assert.False(sut.GivesPermissionTo(null));
Assert.False(sut.Allows(null));
}
[Fact]
public void Should_return_false_if_permission_to_include_is_null()
{
var sut = new PermissionSet(
new Permission("app.contents"),
new Permission("app.assets"));
Assert.False(sut.Includes(null));
}
}
}

2
tests/Squidex.Tests/Pipeline/ApiCostsFilterTests.cs

@ -161,7 +161,7 @@ namespace Squidex.Pipeline
private void SetupApp()
{
httpContext.Features.Set<IAppFeature>(new AppResolverFilter.AppFeature(appEntity));
httpContext.Features.Set<IAppFeature>(new AppResolver.AppFeature(appEntity));
}
}
}

2
tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs

@ -117,7 +117,7 @@ namespace Squidex.Pipeline.CommandMiddlewares
A.CallTo(() => appEntity.Id).Returns(appId);
A.CallTo(() => appEntity.Name).Returns(appName);
httpContext.Features.Set<IAppFeature>(new AppResolverFilter.AppFeature(appEntity));
httpContext.Features.Set<IAppFeature>(new AppResolver.AppFeature(appEntity));
}
}
}

2
tests/Squidex.Tests/Pipeline/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs

@ -208,7 +208,7 @@ namespace Squidex.Pipeline.CommandMiddlewares
A.CallTo(() => appEntity.Id).Returns(appId);
A.CallTo(() => appEntity.Name).Returns(appName);
httpContext.Features.Set<IAppFeature>(new AppResolverFilter.AppFeature(appEntity));
httpContext.Features.Set<IAppFeature>(new AppResolver.AppFeature(appEntity));
}
}
}

Loading…
Cancel
Save