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. 34
      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. 25
      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. 2
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  59. 10
      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;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared;
using System.Linq;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
public static class RoleExtension 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) public static PermissionSet ToPermissions(this AppClientPermission clientPermission, string app)
{ {
Guard.Enum(clientPermission, nameof(clientPermission)); Guard.Enum(clientPermission, nameof(clientPermission));

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

@ -19,6 +19,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet> <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.Caching;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities 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 () => return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () =>
{ {
using (Profiler.TraceMethod<AppProvider>()) using (Profiler.TraceMethod<AppProvider>())
{ {
var ids = await grainFactory.GetGrain<IAppsByUserIndex>(userId).GetAppIdsAsync(); var ids =
await Task.WhenAll(
GetAppIdsByUserAsync(userId),
GetAppIdsAsync(permissions.ToAppNames()));
var apps = var apps =
await Task.WhenAll( await Task.WhenAll(ids
ids.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync())); .SelectMany(x => x)
.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync()));
return apps.Where(a => IsFound(a.Value)).Select(a => a.Value).ToList(); 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) private async Task<Guid> GetAppIdAsync(string name)
{ {
using (Profiler.TraceMethod<AppProvider>()) 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); 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) public Task<Guid> GetAppIdAsync(string appName)
{ {
state.Apps.TryGetValue(appName, out var appId); 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 RemoveReservationAsync(Guid appId, string name);
Task<Guid> GetAppIdAsync(string name);
Task<List<Guid>> GetAppIdsAsync(); 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<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); 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) public void AddClaim(Claim claim)
{ {
Claims.Add(claim); Claims.Add(claim);
@ -173,7 +178,7 @@ namespace Squidex.Domain.Users.MongoDb
public void SetClaim(string type, string value) public void SetClaim(string type, string value)
{ {
Claims.RemoveAll(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase)); RemoveClaims(type);
AddClaim(new Claim(type, value)); AddClaim(new Claim(type, value));
} }

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

@ -45,7 +45,7 @@ namespace Squidex.Domain.Users
return result; 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); var user = factory.Create(email);
@ -54,6 +54,11 @@ namespace Squidex.Domain.Users
user.SetDisplayName(displayName); user.SetDisplayName(displayName);
user.SetPictureUrlFromGravatar(email); user.SetPictureUrlFromGravatar(email);
if (permissions != null)
{
user.SetPermissions(permissions);
}
await DoChecked(() => userManager.CreateAsync(user), "Cannot create user."); await DoChecked(() => userManager.CreateAsync(user), "Cannot create user.");
if (!string.IsNullOrWhiteSpace(password)) if (!string.IsNullOrWhiteSpace(password))
@ -80,7 +85,7 @@ namespace Squidex.Domain.Users
return userManager.UpdateAsync(user); 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); var user = await userManager.FindByIdAsync(id);
@ -100,6 +105,11 @@ namespace Squidex.Domain.Users
user.SetDisplayName(displayName); user.SetDisplayName(displayName);
} }
if (permissions != null)
{
user.SetPermissions(permissions);
}
await DoChecked(() => userManager.UpdateAsync(user), "Cannot update user."); await DoChecked(() => userManager.UpdateAsync(user), "Cannot update user.");
if (!string.IsNullOrWhiteSpace(password)) if (!string.IsNullOrWhiteSpace(password))

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

@ -47,7 +47,9 @@ namespace Squidex.Infrastructure.Security
return null; 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(); .ToArray();
} }

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -16,42 +17,55 @@ namespace Squidex.Infrastructure.Security
public static readonly PermissionSet Empty = new PermissionSet(); public static readonly PermissionSet Empty = new PermissionSet();
private readonly List<Permission> permissions; private readonly List<Permission> permissions;
private readonly Lazy<string> display;
public int Count public int Count
{ {
get { return permissions.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)); Guard.NotNull(permissions, nameof(permissions));
this.permissions = permissions.ToList(); 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) if (other == null)
{ {
return false; return false;
} }
foreach (var permission in permissions) return permissions.Any(x => x.Allows(other));
}
public bool Includes(Permission other)
{ {
if (permission.Allows(other)) if (other == null)
{ {
return true; return false;
} }
return permissions.Any(x => x.Includes(other));
} }
return false; public override string ToString()
{
return display.Value;
}
public IEnumerable<string> ToIds()
{
return permissions.Select(x => x.Id);
} }
public IEnumerator<Permission> GetEnumerator() 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) 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) 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) 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 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:"; 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Security; 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.*"; public const string All = "squidex.*";
@ -113,5 +116,25 @@ namespace Squidex.Domain.Apps.Core
return new Permission(id.Replace("{app}", app ?? "*").Replace("{name}", schema ?? "*")); 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; } IReadOnlyList<ExternalLogin> Logins { get; }
void RemoveClaims(string type);
void SetEmail(string email); void SetEmail(string email);
void SetClaim(string type, string value); void SetClaim(string type, string value);

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

@ -7,6 +7,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Security.Claims;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
@ -16,97 +17,122 @@ namespace Squidex.Shared.Users
{ {
public static void SetDisplayName(this IUser user, string displayName) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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) public static bool HasDisplayName(this IUser user)
{ {
return user.HasClaim(SquidexClaimTypes.SquidexDisplayName); return user.HasClaim(SquidexClaimTypes.DisplayName);
} }
public static bool HasPictureUrl(this IUser user) public static bool HasPictureUrl(this IUser user)
{ {
return user.HasClaim(SquidexClaimTypes.SquidexPictureUrl); return user.HasClaim(SquidexClaimTypes.PictureUrl);
} }
public static bool IsPictureUrlStored(this IUser user) 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) public static string PictureUrl(this IUser user)
{ {
return user.GetClaimValue(SquidexClaimTypes.SquidexPictureUrl); return user.GetClaimValue(SquidexClaimTypes.PictureUrl);
} }
public static string DisplayName(this IUser user) 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) 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")) 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models; 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.Commands;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps namespace Squidex.Areas.Api.Controllers.Apps
{ {

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

@ -8,11 +8,11 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models; 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.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models; 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.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models; 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.Commands;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Apps.Models; using Squidex.Areas.Api.Controllers.Apps.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
using Squidex.Shared.Users;
namespace Squidex.Areas.Api.Controllers.Apps namespace Squidex.Areas.Api.Controllers.Apps
{ {
@ -55,11 +56,12 @@ namespace Squidex.Areas.Api.Controllers.Apps
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> GetApps() 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(); Response.Headers["ETag"] = response.ToManyEtag();

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

@ -7,7 +7,6 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using NodaTime; using NodaTime;
using Squidex.Domain.Apps.Core.Apps; 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.Domain.Apps.Entities.Apps.Services;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Apps.Models namespace Squidex.Areas.Api.Controllers.Apps.Models
{ {
@ -62,11 +62,18 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
public string PlanUpgrade { get; set; } 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()); 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.PlanName = plans.GetPlanForApp(app)?.Name;
response.PlanUpgrade = plans.GetPlanUpgradeForApp(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 NSwag.Annotations;
using Squidex.Areas.Api.Controllers.Assets.Models; using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Areas.Api.Controllers.Contents; using Squidex.Areas.Api.Controllers.Contents;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
@ -25,6 +24,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Assets 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 Microsoft.AspNetCore.Mvc;
using Orleans; using Orleans;
using Squidex.Areas.Api.Controllers.Backups.Models; using Squidex.Areas.Api.Controllers.Backups.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Backups 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 Microsoft.AspNetCore.Mvc;
using Orleans; using Orleans;
using Squidex.Areas.Api.Controllers.Backups.Models; using Squidex.Areas.Api.Controllers.Backups.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Backups 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 Microsoft.AspNetCore.Mvc;
using Orleans; using Orleans;
using Squidex.Areas.Api.Controllers.Comments.Models; using Squidex.Areas.Api.Controllers.Comments.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Comments; using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Comments 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;
using NodaTime.Text; using NodaTime.Text;
using Squidex.Areas.Api.Controllers.Contents.Models; using Squidex.Areas.Api.Controllers.Contents.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Contents; 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.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Contents 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 Microsoft.AspNetCore.Mvc;
using Orleans; using Orleans;
using Squidex.Areas.Api.Controllers.EventConsumers.Models; using Squidex.Areas.Api.Controllers.EventConsumers.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.EventConsumers 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.History.Models; using Squidex.Areas.Api.Controllers.History.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.History.Repositories; using Squidex.Domain.Apps.Entities.History.Repositories;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.History namespace Squidex.Areas.Api.Controllers.History
{ {

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

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

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

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

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

@ -8,10 +8,10 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Schemas.Models; using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Schemas 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Schemas.Models; using Squidex.Areas.Api.Controllers.Schemas.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Schemas 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 System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Statistics.Models; using Squidex.Areas.Api.Controllers.Statistics.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Apps.Services; using Squidex.Domain.Apps.Entities.Apps.Services;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.UsageTracking; using Squidex.Infrastructure.UsageTracking;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
namespace Squidex.Areas.Api.Controllers.Statistics 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> /// </summary>
[Required] [Required]
public string Password { get; set; } 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. /// The password of the user.
/// </summary> /// </summary>
public string Password { get; set; } 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> /// </summary>
[Required] [Required]
public string Id { get; set; } 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] [Required]
public bool IsLocked { get; set; } public bool IsLocked { get; set; }
/// <summary>
/// Additional permissions for the user.
/// </summary>
[Required]
public string[] Permissions { get; set; }
public static UserDto FromUser(IUser user) 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.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Squidex.Areas.Api.Controllers.Users.Models; using Squidex.Areas.Api.Controllers.Users.Models;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Users; using Squidex.Domain.Users;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Pipeline; using Squidex.Pipeline;
using Squidex.Shared;
using Squidex.Shared.Users; using Squidex.Shared.Users;
namespace Squidex.Areas.Api.Controllers.Users namespace Squidex.Areas.Api.Controllers.Users
@ -75,7 +75,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersCreate)] [ApiPermission(Permissions.AdminUsersCreate)]
public async Task<IActionResult> PostUser([FromBody] CreateUserDto request) 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 }; var response = new UserCreatedDto { Id = user.Id };
@ -87,7 +87,7 @@ namespace Squidex.Areas.Api.Controllers.Users
[ApiPermission(Permissions.AdminUsersUpdate)] [ApiPermission(Permissions.AdminUsersUpdate)]
public async Task<IActionResult> PutUser(string id, [FromBody] UpdateUserDto request) 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(); return NoContent();
} }

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

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

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

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

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

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

15
src/Squidex/Pipeline/ApiPermissionAttribute.cs

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

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

@ -10,16 +10,17 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Pipeline namespace Squidex.Pipeline
{ {
public sealed class AppResolverFilter : IAsyncActionFilter public sealed class AppResolver : IAsyncActionFilter
{ {
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
@ -33,7 +34,7 @@ namespace Squidex.Pipeline
} }
} }
public AppResolverFilter(IAppProvider appProvider) public AppResolver(IAppProvider appProvider)
{ {
this.appProvider = appProvider; this.appProvider = appProvider;
} }
@ -42,13 +43,6 @@ namespace Squidex.Pipeline
{ {
var user = context.HttpContext.User; 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(); var appName = context.RouteData.Values["app"]?.ToString();
if (!string.IsNullOrWhiteSpace(appName)) if (!string.IsNullOrWhiteSpace(appName))
@ -66,14 +60,21 @@ namespace Squidex.Pipeline
FindByOpenIdClient(app, user); FindByOpenIdClient(app, user);
if (permissions.Count == 0) if (permissions.Count == 0)
{
var set = new PermissionSet(user.Permissions().Select(x => new Permission(x)));
if (!set.Includes(Permissions.ForApp(Permissions.App, appName)))
{ {
context.Result = new NotFoundResult(); context.Result = new NotFoundResult();
return; return;
} }
}
var identity = user.Identities.First();
foreach (var permission in permissions) 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)); context.HttpContext.Features.Set<IAppFeature>(new AppFeature(app));
@ -96,7 +97,7 @@ namespace Squidex.Pipeline
private static PermissionSet FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user) 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)) 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 NJsonSchema.Generation;
using NSwag; using NSwag;
using Squidex.Config; using Squidex.Config;
using Squidex.Shared.Identity;
namespace Squidex.Pipeline.Swagger 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"> <th class="cell-auto-right">
Position Position
</th> </th>
<th class="cell-actions-lg" *sqxPermission="'squidex.admin.events.manage'"> <th class="cell-actions-lg">
Actions Actions
</th> </th>
</tr> </tr>
@ -41,7 +41,7 @@
<td class="cell-auto-right"> <td class="cell-auto-right">
<span>{{eventConsumer.position}}</span> <span>{{eventConsumer.position}}</span>
</td> </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"> <button class="btn btn-link" (click)="reset(eventConsumer)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer">
<i class="icon icon-reset"></i> <i class="icon icon-reset"></i>
</button> </button>

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

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

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

@ -16,7 +16,7 @@
<ng-container menu> <ng-container menu>
<ng-container *ngIf="usersState.selectedUser | async; else noUserMenu"> <ng-container *ngIf="usersState.selectedUser | async; else noUserMenu">
<ng-container *ngIf="canUpdate"> <ng-container>
<button type="submit" class="btn btn-primary" title="CTRL + S"> <button type="submit" class="btn btn-primary" title="CTRL + S">
Save Save
</button> </button>
@ -49,7 +49,7 @@
<sqx-control-errors for="displayName" [submitted]="userForm.submitted | async"></sqx-control-errors> <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>
<div class="form-group form-group-password" [class.hidden]="user?.isCurrentUser"> <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" /> <input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" autocomplete="false" />
</div> </div>
</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> </ng-container>
</sqx-panel> </sqx-panel>
</form> </form>

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

@ -4,3 +4,7 @@
.form-group-password { .form-group-password {
margin-top: 2rem; 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 { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { AuthService, Permission, permissionsAllow } from '@app/shared';
import { UserDto } from './../../services/users.service'; import { UserDto } from './../../services/users.service';
import { UserForm, UsersState } from './../../state/users.state'; import { UserForm, UsersState } from './../../state/users.state';
const UserUpdatePermission = new Permission('squidex.admin.users.update');
@Component({ @Component({
selector: 'sqx-user-page', selector: 'sqx-user-page',
styleUrls: ['./user-page.component.scss'], styleUrls: ['./user-page.component.scss'],
@ -30,13 +26,12 @@ export class UserPageComponent implements OnDestroy, OnInit {
public user?: { user: UserDto, isCurrentUser: boolean }; public user?: { user: UserDto, isCurrentUser: boolean };
public userForm = new UserForm(this.formBuilder); public userForm = new UserForm(this.formBuilder);
constructor(authService: AuthService, constructor(
public readonly usersState: UsersState, public readonly usersState: UsersState,
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly router: Router private readonly router: Router
) { ) {
this.canUpdate = permissionsAllow(authService.user!.permissions, UserUpdatePermission);
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -52,10 +47,6 @@ export class UserPageComponent implements OnDestroy, OnInit {
if (selectedUser) { if (selectedUser) {
this.userForm.load(selectedUser.user); 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" /> <input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" />
</form> </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 <i class="icon-plus"></i> New
</button> </button>
</ng-container> </ng-container>
@ -37,7 +37,7 @@
<th class="cell-auto"> <th class="cell-auto">
Email Email
</th> </th>
<th class="cell-actions" *ngIf="canLock"> <th class="cell-actions">
Actions Actions
</th> </th>
</tr> </tr>
@ -59,7 +59,7 @@
<td class="cell-auto"> <td class="cell-auto">
<span class="user-email table-cell">{{userInfo.user.email}}</span> <span class="user-email table-cell">{{userInfo.user.email}}</span>
</td> </td>
<td class="cell-actions" *ngIf="canLock"> <td class="cell-actions">
<ng-container *ngIf="!userInfo.isCurrentUser"> <ng-container *ngIf="!userInfo.isCurrentUser">
<button class="btn btn-link" (click)="lock(userInfo.user); $event.stopPropagation();" *ngIf="!userInfo.user.isLocked" title="Lock User"> <button class="btn btn-link" (click)="lock(userInfo.user); $event.stopPropagation();" *ngIf="!userInfo.user.isLocked" title="Lock User">
<i class="icon icon-unlocked"></i> <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 { FormControl } from '@angular/forms';
import { onErrorResumeNext } from 'rxjs/operators'; import { onErrorResumeNext } from 'rxjs/operators';
import {
AuthService,
Permission,
permissionsAllow
} from '@app/shared';
import { UserDto } from './../../services/users.service'; import { UserDto } from './../../services/users.service';
import { UsersState } from './../../state/users.state'; import { UsersState } from './../../state/users.state';
const UserLockPermission = new Permission('squidex.admin.users.lock');
@Component({ @Component({
selector: 'sqx-users-page', selector: 'sqx-users-page',
styleUrls: ['./users-page.component.scss'], styleUrls: ['./users-page.component.scss'],
@ -28,12 +20,9 @@ const UserLockPermission = new Permission('squidex.admin.users.lock');
export class UsersPageComponent implements OnInit { export class UsersPageComponent implements OnInit {
public usersFilter = new FormControl(); public usersFilter = new FormControl();
public canLock: boolean; constructor(
constructor(authService: AuthService,
public readonly usersState: UsersState public readonly usersState: UsersState
) { ) {
this.canLock = permissionsAllow(authService.user!.permissions, UserLockPermission);
} }
public ngOnInit() { public ngOnInit() {

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

@ -56,12 +56,14 @@ describe('UsersService', () => {
id: '123', id: '123',
email: 'mail1@domain.com', email: 'mail1@domain.com',
displayName: 'User1', displayName: 'User1',
permissions: ['Permission1'],
isLocked: true isLocked: true
}, },
{ {
id: '456', id: '456',
email: 'mail2@domain.com', email: 'mail2@domain.com',
displayName: 'User2', displayName: 'User2',
permissions: ['Permission2'],
isLocked: true isLocked: true
} }
] ]
@ -69,8 +71,8 @@ describe('UsersService', () => {
expect(users!).toEqual( expect(users!).toEqual(
new UsersDto(100, [ new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', true), new UserDto('123', 'mail1@domain.com', 'User1', ['Permission1'], true),
new UserDto('456', 'mail2@domain.com', 'User2', true) new UserDto('456', 'mail2@domain.com', 'User2', ['Permission2'], true)
])); ]));
})); }));
@ -95,12 +97,14 @@ describe('UsersService', () => {
id: '123', id: '123',
email: 'mail1@domain.com', email: 'mail1@domain.com',
displayName: 'User1', displayName: 'User1',
permissions: ['Permission1'],
isLocked: true isLocked: true
}, },
{ {
id: '456', id: '456',
email: 'mail2@domain.com', email: 'mail2@domain.com',
displayName: 'User2', displayName: 'User2',
permissions: ['Permission2'],
isLocked: true isLocked: true
} }
] ]
@ -108,8 +112,8 @@ describe('UsersService', () => {
expect(users!).toEqual( expect(users!).toEqual(
new UsersDto(100, [ new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', true), new UserDto('123', 'mail1@domain.com', 'User1', ['Permission1'], true),
new UserDto('456', 'mail2@domain.com', 'User2', true) new UserDto('456', 'mail2@domain.com', 'User2', ['Permission2'], true)
])); ]));
})); }));
@ -131,17 +135,17 @@ describe('UsersService', () => {
id: '123', id: '123',
email: 'mail1@domain.com', email: 'mail1@domain.com',
displayName: 'User1', displayName: 'User1',
pictureUrl: 'path/to/image1', permissions: ['Permission1'],
isLocked: true 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', it('should make post request to create user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { 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; let user: UserDto;
@ -156,13 +160,13 @@ describe('UsersService', () => {
req.flush({ id: '123', pictureUrl: 'path/to/image1' }); 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', it('should make put request to update user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => { 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(); 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 id: string,
public readonly email: string, public readonly email: string,
public readonly displayName: string, public readonly displayName: string,
public readonly permissions: string[],
public readonly isLocked: boolean public readonly isLocked: boolean
) { ) {
super(); super();
@ -45,6 +46,7 @@ export class CreateUserDto {
constructor( constructor(
public readonly email: string, public readonly email: string,
public readonly displayName: string, public readonly displayName: string,
public readonly permissions: string[],
public readonly password: string public readonly password: string
) { ) {
} }
@ -54,6 +56,7 @@ export class UpdateUserDto {
constructor( constructor(
public readonly email: string, public readonly email: string,
public readonly displayName: string, public readonly displayName: string,
public readonly permissions: string[],
public readonly password?: string public readonly password?: string
) { ) {
} }
@ -81,6 +84,7 @@ export class UsersService {
item.id, item.id,
item.email, item.email,
item.displayName, item.displayName,
item.permissions,
item.isLocked); item.isLocked);
}); });
@ -100,6 +104,7 @@ export class UsersService {
body.id, body.id,
body.email, body.email,
body.displayName, body.displayName,
body.permissions,
body.isLocked); body.isLocked);
}), }),
pretifyError('Failed to load user. Please reload.')); pretifyError('Failed to load user. Please reload.'));
@ -116,6 +121,7 @@ export class UsersService {
body.id, body.id,
dto.email, dto.email,
dto.displayName, dto.displayName,
dto.permissions,
false); false);
}), }),
pretifyError('Failed to create user. Please reload.')); 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', () => { describe('UsersState', () => {
const oldUsers = [ const oldUsers = [
new UserDto('id1', 'mail1@mail.de', 'name1', false), new UserDto('id1', 'mail1@mail.de', 'name1', ['Permission1'], false),
new UserDto('id2', 'mail2@mail.de', 'name2', true) 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 authService: IMock<AuthService>;
let dialogs: IMock<DialogService>; let dialogs: IMock<DialogService>;
@ -73,8 +73,8 @@ describe('UsersState', () => {
usersState.select('id1').subscribe(); usersState.select('id1').subscribe();
const newUsers = [ const newUsers = [
new UserDto('id1', 'mail1@mail.de_new', 'name1_new', false), new UserDto('id1', 'mail1@mail.de_new', 'name1_new', ['Permission1_New'], false),
new UserDto('id2', 'mail2@mail.de_new', 'name2_new', true) new UserDto('id2', 'mail2@mail.de_new', 'name2_new', ['Permission2_New'], true)
]; ];
usersService.setup(x => x.getUsers(10, 0, undefined)) usersService.setup(x => x.getUsers(10, 0, undefined))
@ -168,7 +168,7 @@ describe('UsersState', () => {
}); });
it('should update user properties when updated', () => { 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)) usersService.setup(x => x.putUser('id1', request))
.returns(() => of({})); .returns(() => of({}));
@ -184,7 +184,7 @@ describe('UsersState', () => {
}); });
it('should add user to snapshot when created', () => { 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)) usersService.setup(x => x.postUser(request))
.returns(() => of(newUser)); .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.') ValidatorsEx.match('password', 'Passwords must be the same.')
] ]
] ],
permissions: ['']
})); }));
} }
public load(user?: UserDto) { public load(user?: UserDto) {
if (user) { if (user) {
this.form.controls['password'].setValidators(null); this.form.controls['password'].setValidators(null);
super.load({ ...user, permissions: user.permissions.join('\n') });
} else { } else {
this.form.controls['password'].setValidators(Validators.required); 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;
} }
} }

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

@ -46,6 +46,7 @@
</tbody> </tbody>
</table> </table>
<ng-container>
<div class="table-items-footer" *ngIf="(contributorsState.isMaxReached | async) === false"> <div class="table-items-footer" *ngIf="(contributorsState.isMaxReached | async) === false">
<form [formGroup]="assignContributorForm.form" (ngSubmit)="assignContributor()"> <form [formGroup]="assignContributorForm.form" (ngSubmit)="assignContributor()">
<div class="row no-gutters"> <div class="row no-gutters">
@ -69,6 +70,7 @@
</ng-container> </ng-container>
</ng-container> </ng-container>
</ng-container> </ng-container>
</ng-container>
<ng-container sidebar> <ng-container sidebar>
<a class="panel-link" routerLink="history" routerLinkActive="active"> <a class="panel-link" routerLink="history" routerLinkActive="active">

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

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

2
src/Squidex/tsconfig.json

@ -17,7 +17,7 @@
"target": "es5", "target": "es5",
"paths": { "paths": {
"@app*": [ "@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"); .Returns("me@email.com");
A.CallTo(() => user.Claims) 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); 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)); 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] [Fact]
public async Task Should_remove_app_id_from_index() 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(((IEnumerable)sut).OfType<Permission>().ToList(), source);
Assert.Equal(3, source.Count); Assert.Equal(3, source.Count);
Assert.Equal("c;b;a", sut.ToString());
} }
[Fact] [Fact]
@ -39,7 +41,17 @@ namespace Squidex.Infrastructure.Security
new Permission("app.contents"), new Permission("app.contents"),
new Permission("app.assets")); 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] [Fact]
@ -49,7 +61,17 @@ namespace Squidex.Infrastructure.Security
new Permission("app.contents"), new Permission("app.contents"),
new Permission("app.assets")); 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] [Fact]
@ -59,7 +81,17 @@ namespace Squidex.Infrastructure.Security
new Permission("app.contents"), new Permission("app.contents"),
new Permission("app.assets")); 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() 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.Id).Returns(appId);
A.CallTo(() => appEntity.Name).Returns(appName); 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.Id).Returns(appId);
A.CallTo(() => appEntity.Name).Returns(appName); 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