Browse Source

Migrated roles, clients and contributors.

pull/332/head
Sebastian Stehle 7 years ago
parent
commit
2e401dbf92
  1. 3
      src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  2. 6
      src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  3. 3
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  4. 21
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  5. 7
      src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs
  6. 5
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  7. 7
      src/Squidex.Infrastructure/Security/PermissionSet.cs
  8. 6
      src/Squidex.Shared/Identity/ClaimsPrincipalExtensions.cs
  9. 14
      src/Squidex.Shared/Identity/SquidexRoles.cs
  10. 1
      src/Squidex.Shared/Permissions.cs
  11. 9
      src/Squidex.Shared/Users/UserExtensions.cs
  12. 1
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  13. 8
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  14. 4
      src/Squidex/Areas/Api/Controllers/Users/UserManagementController.cs
  15. 23
      src/Squidex/Areas/IdentityServer/Config/IdentityServerExtensions.cs
  16. 3
      src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
  17. 1
      src/Squidex/Areas/IdentityServer/Startup.cs
  18. 6
      src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs
  19. 1
      src/Squidex/Config/Authentication/OidcServices.cs
  20. 3
      src/Squidex/Config/Domain/EntitiesServices.cs
  21. 9
      src/Squidex/Config/Web/WebServices.cs
  22. 7
      src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs
  23. 16
      src/Squidex/Pipeline/ApiPermissionUnifier.cs
  24. 2
      src/Squidex/WebStartup.cs
  25. 6
      src/Squidex/app/features/settings/pages/clients/client.component.html
  26. 6
      src/Squidex/app/features/settings/pages/clients/client.component.ts
  27. 4
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  28. 6
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  29. 2
      src/Squidex/app/features/settings/settings-area.component.html
  30. 6
      src/Squidex/app/shared/services/app-clients.service.spec.ts
  31. 8
      src/Squidex/app/shared/services/app-clients.service.ts
  32. 4
      src/Squidex/app/shared/services/app-contributors.service.spec.ts
  33. 4
      src/Squidex/app/shared/services/app-contributors.service.ts
  34. 4
      src/Squidex/app/shared/state/clients.state.spec.ts
  35. 2
      src/Squidex/app/shared/state/clients.state.ts
  36. 6
      src/Squidex/app/shared/state/contributors.state.ts
  37. 3
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs
  38. 315
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs
  39. 8
      tools/Migrate_01/MigrationPath.cs
  40. 27
      tools/Migrate_01/Migrations/RebuildApps.cs

3
src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs

@ -70,8 +70,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return new Role(Reader,
P.ForApp(P.AppAssetsRead, app),
P.ForApp(P.AppCommon, app),
P.ForApp(P.AppContentsRead, app),
P.ForApp(P.AppContentsGraphQL, app));
P.ForApp(P.AppContentsRead, app));
}
public static Role CreateDeveloper(string app)

6
src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs

@ -19,6 +19,12 @@ namespace Squidex.Domain.Apps.Entities.Apps
public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
{
AddEventMessage("AppContributorAssignedEvent",
"assigned {user:[Contributor]} as {[Role]}");
AddEventMessage("AppClientUpdatedEvent",
"updated client {[Id]}");
AddEventMessage<AppContributorAssigned>(
"assigned {user:[Contributor]} as {[Role]}");

3
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs

@ -7,7 +7,6 @@
using System;
using System.Linq;
using System.Security;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
@ -47,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
if (string.Equals(command.ContributorId, command.Actor?.Identifier, StringComparison.OrdinalIgnoreCase) && !command.FromRestore)
{
throw new SecurityException("You cannot change your own role.");
throw new DomainForbiddenException("You cannot change your own role.");
}
if (contributors.TryGetValue(command.ContributorId, out var existing))

21
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.OData;
using Squidex.Domain.Apps.Core.Contents;
@ -21,6 +22,9 @@ using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.OData;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
#pragma warning disable RECS0147
@ -73,6 +77,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
var schema = await GetSchemaAsync(context);
CheckPermission(schema, context.Base.User);
using (Profiler.TraceMethod<ContentQueryService>())
{
var isVersioned = version > EtagVersion.Empty;
@ -86,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (content == null || (content.Status != Status.Published && !context.Base.IsFrontendClient) || content.SchemaId.Id != schema.Id)
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity));
throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity));
}
return Transform(context.Base, schema, true, content);
@ -99,6 +105,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
var schema = await GetSchemaAsync(context);
CheckPermission(schema, context.Base.User);
using (Profiler.TraceMethod<ContentQueryService>())
{
var status = GetQueryStatus(context.Base);
@ -257,6 +265,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
return schema;
}
private void CheckPermission(ISchemaEntity schema, ClaimsPrincipal user)
{
var permissions = user.Permissions();
var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.Name);
if (!permissions.Allows(permission))
{
throw new DomainForbiddenException("You do not have permission for this schema.");
}
}
private static Status[] GetFindStatus(QueryContext context)
{
if (context.IsFrontendClient)

7
src/Squidex.Domain.Apps.Entities/History/HistoryEventsCreatorBase.cs

@ -36,6 +36,13 @@ namespace Squidex.Domain.Apps.Entities.History
texts[typeNameRegistry.GetName<TEvent>()] = message;
}
protected void AddEventMessage(string type, string message)
{
Guard.NotNullOrEmpty(message, nameof(message));
texts[type] = message;
}
protected bool HasEventText(IEvent @event)
{
var message = typeNameRegistry.GetName(@event.GetType());

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

@ -11,6 +11,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using Squidex.Shared.Users;
namespace Squidex.Domain.Users
@ -45,7 +46,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, string[] permissions = null)
public static async Task<IUser> CreateAsync(this UserManager<IUser> userManager, IUserFactory factory, string email, string displayName, string password, PermissionSet permissions = null)
{
var user = factory.Create(email);
@ -85,7 +86,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, string[] permissions = null)
public static async Task UpdateAsync(this UserManager<IUser> userManager, string id, string email, string displayName, string password, PermissionSet permissions = null)
{
var user = await userManager.FindByIdAsync(id);

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

@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.Security
{
public sealed class PermissionSet : IReadOnlyCollection<Permission>
{
public static readonly PermissionSet Empty = new PermissionSet();
public static readonly PermissionSet Empty = new PermissionSet(new string[0]);
private readonly List<Permission> permissions;
private readonly Lazy<string> display;
@ -29,6 +29,11 @@ namespace Squidex.Infrastructure.Security
{
}
public PermissionSet(params string[] permissions)
: this(permissions?.Select(x => new Permission(x)))
{
}
public PermissionSet(IEnumerable<string> permissions)
: this(permissions?.Select(x => new Permission(x)))
{

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

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Squidex.Infrastructure.Security;
namespace Squidex.Shared.Identity
{
@ -24,6 +25,11 @@ namespace Squidex.Shared.Identity
identity.AddClaim(new Claim(SquidexClaimTypes.PictureUrl, pictureUrl));
}
public static PermissionSet Permissions(this ClaimsPrincipal principal)
{
return new PermissionSet(principal.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).Select(x => new Permission(x.Value)));
}
public static IEnumerable<Claim> GetSquidexClaims(this ClaimsPrincipal principal)
{
return principal.Claims.Where(c => c.Type.StartsWith(SquidexClaimTypes.Prefix, StringComparison.Ordinal));

14
src/Squidex.Shared/Identity/SquidexRoles.cs

@ -1,14 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Shared.Identity
{
public static class SquidexRoles
{
public static readonly string Administrator = "ADMINISTRATOR";
}
}

1
src/Squidex.Shared/Permissions.cs

@ -16,6 +16,7 @@ namespace Squidex.Shared
public const string All = "squidex.*";
public const string Admin = "squidex.admin.*";
public const string AdminOrleans = "squidex.admin.orleans";
public const string AdminRestore = "squidex.admin.restore";
public const string AdminRestoreRead = "squidex.admin.restore.read";

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

@ -51,13 +51,13 @@ namespace Squidex.Shared.Users
user.SetClaim(SquidexClaimTypes.ConsentForEmails, value.ToString());
}
public static void SetPermissions(this IUser user, params string[] permissions)
public static void SetPermissions(this IUser user, PermissionSet permissions)
{
user.RemoveClaims(SquidexClaimTypes.Permissions);
foreach (var permission in permissions)
{
user.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission));
user.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission.Id));
}
}
@ -101,11 +101,6 @@ namespace Squidex.Shared.Users
return user.GetClaimValue(SquidexClaimTypes.DisplayName);
}
public static PermissionSet Permissions(this ClaimsPrincipal principal)
{
return new PermissionSet(principal.Claims.Where(x => x.Type == SquidexClaimTypes.Permissions).Select(x => new Permission(x.Value)));
}
public static PermissionSet Permissions(this IUser user)
{
return new PermissionSet(user.GetClaimValues(SquidexClaimTypes.Permissions).Select(x => new Permission(x)));

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

@ -17,6 +17,7 @@ using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Security;
using Squidex.Pipeline;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Areas.Api.Controllers.Apps

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

@ -57,7 +57,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[HttpGet]
[HttpPost]
[Route("content/{app}/graphql/")]
[ApiPermission(Permissions.AppContentsGraphQL)]
[ApiPermission]
[ApiCosts(2)]
public async Task<IActionResult> PostGraphQL(string app, [FromBody] GraphQLQuery query)
{
@ -88,7 +88,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
[HttpGet]
[HttpPost]
[Route("content/{app}/graphql/batch")]
[ApiPermission(Permissions.AppContentsGraphQL)]
[ApiPermission]
[ApiCosts(2)]
public async Task<IActionResult> PostGraphQLBatch(string app, [FromBody] GraphQLQuery[] batch)
{
@ -120,7 +120,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpGet]
[Route("content/{app}/{name}/")]
[ApiPermission(Permissions.AppContentsRead)]
[ApiPermission]
[ApiCosts(2)]
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] bool archived = false, [FromQuery] string ids = null)
{
@ -159,7 +159,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// </remarks>
[HttpGet]
[Route("content/{app}/{name}/{id}/")]
[ApiPermission(Permissions.AppContentsRead)]
[ApiPermission]
[ApiCosts(1)]
public async Task<IActionResult> GetContent(string app, string name, Guid id)
{

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

@ -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, request.Permissions);
var user = await userManager.CreateAsync(userFactory, request.Email, request.DisplayName, request.Password, new PermissionSet(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, request.Permissions);
await userManager.UpdateAsync(id, request.Email, request.DisplayName, request.Password, new PermissionSet(request.Permissions));
return NoContent();
}

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

@ -15,8 +15,8 @@ using Microsoft.Extensions.Options;
using Squidex.Config;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Areas.IdentityServer.Config
@ -30,23 +30,6 @@ namespace Squidex.Areas.IdentityServer.Config
return app;
}
public static IServiceProvider UseMyAdminRole(this IServiceProvider services)
{
var roleManager = services.GetRequiredService<RoleManager<IRole>>();
Task.Run(async () =>
{
if (!await roleManager.RoleExistsAsync(SquidexRoles.Administrator))
{
var role = services.GetRequiredService<IRoleFactory>().Create(SquidexRoles.Administrator);
await roleManager.CreateAsync(role);
}
}).Wait();
return services;
}
public static IServiceProvider UseMyAdmin(this IServiceProvider services)
{
var options = services.GetService<IOptions<MyIdentityOptions>>().Value;
@ -67,7 +50,9 @@ namespace Squidex.Areas.IdentityServer.Config
{
try
{
await userManager.CreateAsync(userFactory, adminEmail, adminEmail, adminPass, new[] { Permissions.Admin });
var permissions = new PermissionSet(Permissions.Admin);
await userManager.CreateAsync(userFactory, adminEmail, adminEmail, adminPass, permissions);
}
catch (Exception ex)
{

3
src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs

@ -22,6 +22,7 @@ using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
@ -315,7 +316,7 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
return TaskHelper.True;
}
return MakeIdentityOperation(() => userManager.AddToRoleAsync(user, SquidexRoles.Administrator));
return MakeIdentityOperation(() => userManager.AddClaimAsync(user, new Claim(SquidexClaimTypes.Permissions, Permissions.Admin)));
}
private IUser CreateUser(ExternalLoginInfo externalLogin, string email)

1
src/Squidex/Areas/IdentityServer/Startup.cs

@ -17,7 +17,6 @@ namespace Squidex.Areas.IdentityServer
{
public static void ConfigureIdentityServer(this IApplicationBuilder app)
{
app.ApplicationServices.UseMyAdminRole();
app.ApplicationServices.UseMyAdmin();
var environment = app.ApplicationServices.GetRequiredService<IHostingEnvironment>();

6
src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs

@ -10,12 +10,16 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
namespace Squidex.Areas.OrleansDashboard.Middlewares
{
public sealed class OrleansDashboardAuthenticationMiddleware
{
private static readonly Permission OrleansPermissions = new Permission(Permissions.AdminOrleans);
private readonly RequestDelegate next;
public OrleansDashboardAuthenticationMiddleware(RequestDelegate next)
@ -27,7 +31,7 @@ namespace Squidex.Areas.OrleansDashboard.Middlewares
{
var authentication = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (!authentication.Succeeded || !authentication.Principal.IsInRole(SquidexRoles.Administrator))
if (!authentication.Succeeded || !authentication.Principal.Permissions().Allows(OrleansPermissions))
{
await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties
{

1
src/Squidex/Config/Authentication/OidcServices.cs

@ -24,6 +24,7 @@ namespace Squidex.Config.Authentication
options.Authority = identityOptions.OidcAuthority;
options.ClientId = identityOptions.OidcClient;
options.ClientSecret = identityOptions.OidcSecret;
options.Scope.Add(Constants.PermissionsScope);
options.RequireHttpsMetadata = false;
});
}

3
src/Squidex/Config/Domain/EntitiesServices.cs

@ -243,6 +243,9 @@ namespace Squidex.Config.Domain
services.AddTransientAs<RebuildSnapshots>()
.As<IMigration>();
services.AddTransientAs<RebuildApps>()
.As<IMigration>();
services.AddTransientAs<RebuildAssets>()
.As<IMigration>();

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Config.Domain;
using Squidex.Pipeline;
@ -18,10 +19,10 @@ namespace Squidex.Config.Web
services.AddSingletonAs<FileCallbackResultExecutor>()
.AsSelf();
services.AddSingletonAs<AppResolver>()
services.AddSingletonAs<ApiCostsFilter>()
.AsSelf();
services.AddSingletonAs<ApiCostsFilter>()
services.AddSingletonAs<AppResolver>()
.AsSelf();
services.AddSingletonAs<EnforceHttpsMiddleware>()
@ -33,10 +34,12 @@ namespace Squidex.Config.Web
services.AddSingletonAs<RequestLogPerformanceMiddleware>()
.AsSelf();
services.AddSingletonAs<ApiPermissionUnifier>()
.As<IClaimsTransformation>();
services.AddMvc(options =>
{
options.Filters.Add<ETagFilter>();
options.Filters.Add<ApiPermissionUnifier>();
options.Filters.Add<AppResolver>();
}).AddMySerializers();

7
src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Infrastructure;
@ -30,6 +31,7 @@ namespace Squidex.Pipeline
AddHandler<DomainObjectVersionException>(OnDomainObjectVersionException);
AddHandler<DomainForbiddenException>(OnDomainForbiddenException);
AddHandler<DomainException>(OnDomainException);
AddHandler<SecurityException>(OnSecurityException);
}
private static IActionResult OnDomainObjectNotFoundException(DomainObjectNotFoundException ex)
@ -52,6 +54,11 @@ namespace Squidex.Pipeline
return ErrorResult(403, new ErrorDto { Message = ex.Message });
}
private static IActionResult OnSecurityException(SecurityException ex)
{
return ErrorResult(403, new ErrorDto { Message = ex.Message });
}
private static IActionResult OnValidationException(ValidationException ex)
{
return ErrorResult(400, new ErrorDto { Message = ex.Summary, Details = ex.Errors?.Select(e => e.Message).ToArray() });

16
src/Squidex/Pipeline/ApiPermissionUnifier.cs

@ -8,26 +8,26 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Authentication;
using Squidex.Shared;
using Squidex.Shared.Identity;
namespace Squidex.Pipeline
{
public sealed class ApiPermissionUnifier : IAsyncActionFilter
public sealed class ApiPermissionUnifier : IClaimsTransformation
{
public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var user = context.HttpContext.User;
private const string AdministratorRole = "ADMINISTRATOR";
var identity = user.Identities.First();
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = principal.Identities.First();
if (string.Equals(identity.FindFirst(identity.RoleClaimType)?.Value, SquidexRoles.Administrator))
if (string.Equals(identity.FindFirst(identity.RoleClaimType)?.Value, AdministratorRole))
{
identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.Admin));
}
return next();
return Task.FromResult(principal);
}
}
}

2
src/Squidex/WebStartup.cs

@ -41,10 +41,10 @@ namespace Squidex
{
app.ApplicationServices.LogConfiguration();
app.UseMyTracking();
app.UseMyLocalCache();
app.UseMyCors();
app.UseMyForwardingRules();
app.UseMyTracking();
app.ConfigureApi();
app.ConfigurePortal();

6
src/Squidex/app/features/settings/pages/clients/client.component.html

@ -67,11 +67,11 @@
</div>
<div class="row no-gutters">
<div class="col-4 col-form-label">
Permission:
Role:
</div>
<div class="col">
<select class="form-control" [ngModel]="client.permission" (ngModelChange)="update($event)">
<option *ngFor="let permission of clientPermissions" [ngValue]="permission">{{permission}}</option>
<select class="form-control" [ngModel]="client.role" (ngModelChange)="update($event)">
<option *ngFor="let role of clientRoles" [ngValue]="role">{{role}}</option>
</select>
</div>
<div class="col col-auto cell-actions">

6
src/Squidex/app/features/settings/pages/clients/client.component.ts

@ -32,7 +32,7 @@ export class ClientComponent implements OnChanges {
@Input()
public client: AppClientDto;
public clientPermissions = [ 'Developer', 'Editor', 'Reader' ];
public clientRoles = [ 'Owner', 'Developer', 'Editor', 'Reader' ];
public isRenaming = false;
@ -58,8 +58,8 @@ export class ClientComponent implements OnChanges {
this.clientsState.revoke(this.client).pipe(onErrorResumeNext()).subscribe();
}
public update(permission: string) {
this.clientsState.update(this.client, new UpdateAppClientDto(undefined, permission)).pipe(onErrorResumeNext()).subscribe();
public update(role: string) {
this.clientsState.update(this.client, new UpdateAppClientDto(undefined, role)).pipe(onErrorResumeNext()).subscribe();
}
public toggleRename() {

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

@ -32,8 +32,8 @@
<span class="user-name table-cell">{{contributorInfo.contributor.contributorId | sqxUserName}}</span>
</td>
<td class="cell-time">
<select class="form-control" [ngModel]="contributorInfo.contributor.permission" (ngModelChange)="changePermission(contributorInfo.contributor, $event)" [disabled]="contributorInfo.isCurrentUser">
<option *ngFor="let permission of usersPermissions" [ngValue]="permission">{{permission}}</option>
<select class="form-control" [ngModel]="contributorInfo.contributor.role" (ngModelChange)="changeRole(contributorInfo.contributor, $event)" [disabled]="contributorInfo.isCurrentUser">
<option *ngFor="let role of usersRoles" [ngValue]="role">{{role}}</option>
</select>
</td>
<td class="cell-actions">

6
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -53,7 +53,7 @@ export class UsersDataSource implements AutocompleteSource {
]
})
export class ContributorsPageComponent implements OnInit {
public usersPermissions = [ 'Owner', 'Developer', 'Editor' ];
public usersRoles = [ 'Owner', 'Developer', 'Editor', 'Reader' ];
public assignContributorForm = new AssignContributorForm(this.formBuilder);
@ -77,8 +77,8 @@ export class ContributorsPageComponent implements OnInit {
this.contributorsState.revoke(contributor).pipe(onErrorResumeNext()).subscribe();
}
public changePermission(contributor: AppContributorDto, permission: string) {
this.contributorsState.assign(new AppContributorDto(contributor.contributorId, permission)).pipe(onErrorResumeNext()).subscribe();
public changeRole(contributor: AppContributorDto, role: string) {
this.contributorsState.assign(new AppContributorDto(contributor.contributorId, role)).pipe(onErrorResumeNext()).subscribe();
}
public assignContributor() {

2
src/Squidex/app/features/settings/settings-area.component.html

@ -26,7 +26,7 @@
</a>
</li>
<li class="nav-item" *sqxPermission="'squidex.apps.{app}.languages.read'">
<a class="nav-link" routerLink="clients" routerLinkActive="active">
<a class="nav-link" routerLink="languages" routerLinkActive="active">
Languages
<i class="icon-angle-right"></i>
</a>

6
src/Squidex/app/shared/services/app-clients.service.spec.ts

@ -59,13 +59,13 @@ describe('AppClientsService', () => {
id: 'client1',
name: 'Client 1',
secret: 'secret1',
permission: 'Editor'
role: 'Editor'
},
{
id: 'client2',
name: 'Client 2',
secret: 'secret2',
permission: 'Developer'
role: 'Developer'
}
], {
headers: {
@ -96,7 +96,7 @@ describe('AppClientsService', () => {
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toEqual(version.value);
req.flush({ id: 'client1', name: 'Client 1', secret: 'secret1', permission: 'Developer' });
req.flush({ id: 'client1', name: 'Client 1', secret: 'secret1', role: 'Developer' });
expect(client!).toEqual(new AppClientDto('client1', 'Client 1', 'secret1', 'Developer'));
}));

8
src/Squidex/app/shared/services/app-clients.service.ts

@ -34,7 +34,7 @@ export class AppClientDto extends Model {
public readonly id: string,
public readonly name: string,
public readonly secret: string,
public readonly permission: string
public readonly role: string
) {
super();
}
@ -54,7 +54,7 @@ export class CreateAppClientDto {
export class UpdateAppClientDto {
constructor(
public readonly name?: string,
public readonly permission?: string
public readonly role?: string
) {
}
}
@ -90,7 +90,7 @@ export class AppClientsService {
item.id,
item.name || body.id,
item.secret,
item.permission);
item.role);
});
return new AppClientsDto(clients, response.version);
@ -109,7 +109,7 @@ export class AppClientsService {
body.id,
body.name || body.id,
body.secret,
body.permission);
body.role);
return new Versioned(response.version, client);
}),

4
src/Squidex/app/shared/services/app-contributors.service.spec.ts

@ -56,11 +56,11 @@ describe('AppContributorsService', () => {
contributors: [
{
contributorId: '123',
permission: 'Owner'
role: 'Owner'
},
{
contributorId: '456',
permission: 'Owner'
role: 'Owner'
}
],
maxContributors: 100

4
src/Squidex/app/shared/services/app-contributors.service.ts

@ -33,7 +33,7 @@ export class AppContributorsDto extends Model {
export class AppContributorDto extends Model {
constructor(
public readonly contributorId: string,
public readonly permission: string
public readonly role: string
) {
super();
}
@ -68,7 +68,7 @@ export class AppContributorsService {
items.map(item => {
return new AppContributorDto(
item.contributorId,
item.permission);
item.role);
}),
body.maxContributors, response.version);
}),

4
src/Squidex/app/shared/state/clients.state.spec.ts

@ -84,7 +84,7 @@ describe('ClientsState', () => {
});
it('should update properties when updated', () => {
const request = new UpdateAppClientDto('NewName', 'NewPermission');
const request = new UpdateAppClientDto('NewName', 'NewRole');
clientsService.setup(x => x.putClient(app, oldClients[0].id, request, version))
.returns(() => of(new Versioned<any>(newVersion, {})));
@ -94,7 +94,7 @@ describe('ClientsState', () => {
const client_1 = clientsState.snapshot.clients.at(0);
expect(client_1.name).toBe('NewName');
expect(client_1.permission).toBe('NewPermission');
expect(client_1.role).toBe('NewRole');
expect(clientsState.snapshot.version).toEqual(newVersion);
});

2
src/Squidex/app/shared/state/clients.state.ts

@ -118,4 +118,4 @@ export class ClientsState extends State<Snapshot> {
}
const update = (client: AppClientDto, request: UpdateAppClientDto) =>
client.with({ name: request.name || client.name, permission: request.permission || client.permission });
client.with({ name: request.name || client.name, role: request.role || client.role });

6
src/Squidex/app/shared/state/contributors.state.ts

@ -98,7 +98,7 @@ export class ContributorsState extends State<Snapshot> {
public assign(request: AppContributorDto): Observable<any> {
return this.appContributorsService.postContributor(this.appName, request, this.version).pipe(
tap(dto => {
const contributors = this.updateContributors(dto.payload.contributorId, request.permission, dto.version);
const contributors = this.updateContributors(dto.payload.contributorId, request.role, dto.version);
this.replaceContributors(contributors, dto.version);
}),
@ -112,8 +112,8 @@ export class ContributorsState extends State<Snapshot> {
notify(this.dialogs));
}
private updateContributors(id: string, permission: string, version: Version) {
const contributor = new AppContributorDto(id, permission);
private updateContributors(id: string, role: string, version: Version) {
const contributor = new AppContributorDto(id, role);
const contributors = this.snapshot.contributors;
if (contributors.find(x => x.contributor.contributorId === id)) {

3
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core.Apps;
@ -92,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new AssignContributor { ContributorId = "3", Role = Role.Editor, Actor = new RefToken("user", "3") };
return Assert.ThrowsAsync<SecurityException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan));
return Assert.ThrowsAsync<DomainForbiddenException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan));
}
[Fact]

315
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs

@ -23,6 +23,8 @@ using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Xunit;
#pragma warning disable SA1401 // Fields must be private
@ -39,7 +41,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly Guid appId = Guid.NewGuid();
private readonly Guid schemaId = Guid.NewGuid();
private readonly Guid contentId = Guid.NewGuid();
private readonly string appName = "my-app";
private readonly string schemaName = "my-schema";
private readonly string script = "<script-query>";
private readonly NamedContentData contentData = new NamedContentData();
private readonly NamedContentData contentTransformed = new NamedContentData();
private readonly ClaimsPrincipal user;
@ -56,7 +61,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => app.Name).Returns(appName);
A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.English);
A.CallTo(() => schema.SchemaDef).Returns(new Schema("my-schema"));
A.CallTo(() => schema.AppId).Returns(new NamedId<Guid>(appId, appName));
A.CallTo(() => schema.Id).Returns(schemaId);
A.CallTo(() => schema.Name).Returns(schemaName);
A.CallTo(() => schema.SchemaDef).Returns(new Schema(schemaName));
A.CallTo(() => schema.ScriptQuery).Returns(script);
context = new ContentQueryContext(QueryContext.Create(app, user));
@ -66,8 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_return_schema_from_id_if_string_is_guid()
{
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
SetupSchema();
var result = await sut.GetSchemaAsync(context.WithSchemaId(schemaId));
@ -77,52 +85,76 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact]
public async Task Should_return_schema_from_name_if_string_not_guid()
{
A.CallTo(() => appProvider.GetSchemaAsync(appId, "my-schema"))
.Returns(schema);
SetupSchema();
var result = await sut.GetSchemaAsync(context.WithSchemaName("my-schema"));
var result = await sut.GetSchemaAsync(context.WithSchemaName(schemaName));
Assert.Equal(schema, result);
}
[Fact]
public async Task Should_throw_if_schema_not_found()
public async Task Should_throw_404_if_schema_not_found()
{
A.CallTo(() => appProvider.GetSchemaAsync(appId, "my-schema"))
.Returns((ISchemaEntity)null);
SetupSchemaNotFound();
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSchemaAsync(context.WithSchemaName("my-schema")));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSchemaAsync(context.WithSchemaName(schemaName)));
}
[Fact]
public async Task Should_throw_if_schema_not_found_in_check()
public async Task Should_throw_404_if_schema_not_found_in_check()
{
A.CallTo(() => appProvider.GetSchemaAsync(appId, "my-schema"))
.Returns((ISchemaEntity)null);
SetupSchemaNotFound();
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ThrowIfSchemaNotExistsAsync(context.WithSchemaName("my-schema")));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.ThrowIfSchemaNotExistsAsync(context.WithSchemaName(schemaName)));
}
public static IEnumerable<object[]> SingleRequestData = new[]
public static IEnumerable<object[]> SingleDataFrontend = new[]
{
new object[] { true, true, new[] { Status.Archived, Status.Draft, Status.Published } },
new object[] { true, false, new[] { Status.Archived, Status.Draft, Status.Published } },
new object[] { false, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, new[] { Status.Published } }
new object[] { true, new[] { Status.Archived, Status.Draft, Status.Published } },
new object[] { false, new[] { Status.Archived, Status.Draft, Status.Published } }
};
public static IEnumerable<object[]> SingleDataApi = new[]
{
new object[] { true, new[] { Status.Draft, Status.Published } },
new object[] { false, new[] { Status.Published } }
};
[Fact]
public async Task Should_throw_for_single_content_if_no_permission()
{
SetupClaims(false, false);
SetupSchema();
var ctx = context.WithSchemaId(schemaId);
await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.FindContentAsync(ctx, contentId));
}
[Fact]
public async Task Should_throw_404_for_single_content_if_content_not_found()
{
SetupClaims();
SetupSchema();
A.CallTo(() => contentRepository.FindContentAsync(app, schema, new[] { Status.Published }, contentId))
.Returns((IContentEntity)null);
var ctx = context.WithSchemaId(schemaId);
await Assert.ThrowsAsync<DomainObjectNotFoundException>(async () => await sut.FindContentAsync(ctx, contentId));
}
[Theory]
[MemberData(nameof(SingleRequestData))]
public async Task Should_return_content_from_repository_and_transform(bool isFrontend, bool unpublished, params Status[] status)
[MemberData(nameof(SingleDataFrontend))]
public async Task Should_return_single_content_for_frontend_without_transform(bool unpublished, params Status[] status)
{
var contentId = Guid.NewGuid();
var content = CreateContent(contentId);
SetupClaims(isFrontend);
SetupClaims(true);
SetupSchema();
SetupScripting(contentId);
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
A.CallTo(() => contentRepository.FindContentAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), contentId))
.Returns(content);
@ -132,70 +164,94 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Equal(contentTransformed, result.Data);
Assert.Equal(content.Id, result.Id);
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_return_versioned_content_from_repository_and_transform()
[Theory]
[MemberData(nameof(SingleDataApi))]
public async Task Should_return_single_content_for_api_with_transform(bool unpublished, params Status[] status)
{
var contentId = Guid.NewGuid();
var content = CreateContent(contentId);
SetupClaims();
SetupSchema();
SetupScripting(contentId);
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
A.CallTo(() => contentVersionLoader.LoadAsync(contentId, 10))
A.CallTo(() => contentRepository.FindContentAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), contentId))
.Returns(content);
var result = await sut.FindContentAsync(context.WithSchemaId(schemaId), contentId, 10);
var ctx = context.WithSchemaId(schemaId).WithUnpublished(unpublished);
var result = await sut.FindContentAsync(ctx, contentId);
Assert.Equal(contentTransformed, result.Data);
Assert.Equal(content.Id, result.Id);
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.Ignored, A<string>.Ignored))
.MustHaveHappened(Repeated.Exactly.Once);
}
[Fact]
public async Task Should_throw_if_content_to_find_does_not_exist()
public async Task Should_return_versioned_content_from_repository_and_transform()
{
var contentId = Guid.NewGuid();
var content = CreateContent(contentId);
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
SetupClaims(true);
SetupSchema();
SetupScripting(contentId);
A.CallTo(() => contentRepository.FindContentAsync(app, schema, new[] { Status.Published }, contentId))
.Returns((IContentEntity)null);
A.CallTo(() => contentVersionLoader.LoadAsync(contentId, 10))
.Returns(content);
var ctx = context.WithSchemaId(schemaId);
var result = await sut.FindContentAsync(ctx, contentId, 10);
await Assert.ThrowsAsync<DomainObjectNotFoundException>(async () => await sut.FindContentAsync(context.WithSchemaId(schemaId), contentId));
Assert.Equal(contentTransformed, result.Data);
Assert.Equal(content.Id, result.Id);
}
public static IEnumerable<object[]> ManyRequestData = new[]
public static IEnumerable<object[]> ManyDataFrontend = new[]
{
new object[] { true, true, new[] { Status.Archived } },
new object[] { true, false, new[] { Status.Archived } },
new object[] { false, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, new[] { Status.Draft, Status.Published } }
};
public static IEnumerable<object[]> ManyDataApi = new[]
{
new object[] { true, true, true, new[] { Status.Archived } },
new object[] { true, true, false, new[] { Status.Archived } },
new object[] { true, false, true, new[] { Status.Draft, Status.Published } },
new object[] { true, false, false, new[] { Status.Draft, Status.Published } },
new object[] { false, true, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, false, new[] { Status.Published } },
new object[] { false, true, false, new[] { Status.Published } }
new object[] { true, true, new[] { Status.Draft, Status.Published } },
new object[] { false, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, new[] { Status.Published } },
new object[] { true, false, new[] { Status.Published } }
};
[Fact]
public async Task Should_throw_for_query_if_no_permission()
{
SetupClaims(false, false);
SetupSchema();
var ctx = context.WithSchemaId(schemaId);
await Assert.ThrowsAsync<DomainForbiddenException>(() => sut.QueryAsync(ctx, Q.Empty));
}
[Theory]
[MemberData(nameof(ManyRequestData))]
public async Task Should_query_contents_by_query_from_repository_and_transform(bool isFrontend, bool archive, bool unpublished, params Status[] status)
[MemberData(nameof(ManyDataFrontend))]
public async Task Should_query_contents_by_query_for_frontend_without_transform(bool archive, bool unpublished, params Status[] status)
{
const int count = 5, total = 200;
var contentId = Guid.NewGuid();
var content = CreateContent(contentId);
SetupClaims(isFrontend);
SetupClaims(true);
SetupSchema();
SetupScripting(contentId);
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<Query>.Ignored))
.Returns(ResultList.Create(total, Enumerable.Repeat(content, count)));
SetupContents(status, count, total, content);
var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished);
@ -206,23 +262,41 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Equal(total, result.Total);
if (!isFrontend)
{
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.Ignored, A<string>.Ignored))
.MustHaveHappened(Repeated.Exactly.Times(count));
.MustNotHaveHappened();
}
else
[Theory]
[MemberData(nameof(ManyDataApi))]
public async Task Should_query_contents_by_query_for_api_and_transform(bool archive, bool unpublished, params Status[] status)
{
const int count = 5, total = 200;
var content = CreateContent(contentId);
SetupClaims();
SetupSchema();
SetupScripting(contentId);
SetupContents(status, count, total, content);
var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished);
var result = await sut.QueryAsync(ctx, Q.Empty);
Assert.Equal(contentData, result[0].Data);
Assert.Equal(contentId, result[0].Id);
Assert.Equal(total, result.Total);
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
.MustHaveHappened(Repeated.Exactly.Times(count));
}
[Fact]
public Task Should_throw_if_query_is_invalid()
{
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
SetupClaims();
SetupSchema();
A.CallTo(() => modelBuilder.BuildEdmModel(schema, app))
.Throws(new ODataException());
@ -230,34 +304,58 @@ namespace Squidex.Domain.Apps.Entities.Contents
return Assert.ThrowsAsync<ValidationException>(() => sut.QueryAsync(context.WithSchemaId(schemaId), Q.Empty.WithODataQuery("query")));
}
public static IEnumerable<object[]> ManyIdRequestData = new[]
public static IEnumerable<object[]> ManyIdDataFrontend = new[]
{
new object[] { true, true, true, new[] { Status.Archived } },
new object[] { true, true, false, new[] { Status.Archived } },
new object[] { true, false, true, new[] { Status.Draft, Status.Published } },
new object[] { true, false, false, new[] { Status.Draft, Status.Published } },
new object[] { false, true, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, false, new[] { Status.Published } },
new object[] { false, true, false, new[] { Status.Published } }
new object[] { true, true, new[] { Status.Archived } },
new object[] { true, false, new[] { Status.Archived } },
new object[] { false, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, new[] { Status.Draft, Status.Published } }
};
public static IEnumerable<object[]> ManyIdDataApi = new[]
{
new object[] { true, true, new[] { Status.Draft, Status.Published } },
new object[] { false, true, new[] { Status.Draft, Status.Published } },
new object[] { false, false, new[] { Status.Published } },
new object[] { true, false, new[] { Status.Published } }
};
[Theory]
[MemberData(nameof(ManyIdRequestData))]
public async Task Should_query_contents_by_id_from_repository_and_transform(bool isFrontend, bool archive, bool unpublished, params Status[] status)
[MemberData(nameof(ManyIdDataFrontend))]
public async Task Should_query_contents_by_id_for_frontend_and_transform(bool archive, bool unpublished, params Status[] status)
{
const int count = 5, total = 200;
var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList();
SetupClaims(isFrontend);
SetupClaims(true);
SetupSchema();
SetupScripting(ids.ToArray());
SetupContents(status, total, ids);
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished);
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<HashSet<Guid>>.Ignored))
.Returns(ResultList.Create(total, ids.Select(x => CreateContent(x)).Shuffle()));
var result = await sut.QueryAsync(ctx, Q.Empty.WithIds(ids));
Assert.Equal(ids, result.Select(x => x.Id).ToList());
Assert.Equal(total, result.Total);
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
[Theory]
[MemberData(nameof(ManyIdDataApi))]
public async Task Should_query_contents_by_id_from_repository_and_transform(bool archive, bool unpublished, params Status[] status)
{
const int count = 5, total = 200;
var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList();
SetupClaims();
SetupSchema();
SetupScripting(ids.ToArray());
SetupContents(status, total, ids);
var ctx = context.WithSchemaId(schemaId).WithArchived(archive).WithUnpublished(unpublished);
@ -266,38 +364,60 @@ namespace Squidex.Domain.Apps.Entities.Contents
Assert.Equal(ids, result.Select(x => x.Id).ToList());
Assert.Equal(total, result.Total);
if (!isFrontend)
{
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.Ignored, A<string>.Ignored))
.MustHaveHappened(Repeated.Exactly.Times(count));
}
else
private void SetupClaims(bool isFrontend = false, bool allowSchema = true)
{
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
if (isFrontend)
{
identity.AddClaim(new Claim(OpenIdClaims.ClientId, "squidex-frontend"));
}
if (allowSchema)
{
identity.AddClaim(new Claim(SquidexClaimTypes.Permissions, Permissions.ForApp(Permissions.AppContentsRead, app.Name, schema.Name).Id));
}
}
private void SetupClaims(bool isFrontend)
private void SetupScripting(params Guid[] ids)
{
if (isFrontend)
foreach (var id in ids)
{
identity.AddClaim(new Claim(OpenIdClaims.ClientId, "squidex-frontend"));
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == id && x.Data == contentData), script))
.Returns(contentTransformed);
}
}
private void SetupScripting(params Guid[] contentId)
private void SetupContents(Status[] status, int count, int total, IContentEntity content)
{
var script = "<script-query>";
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<Query>.Ignored))
.Returns(ResultList.Create(total, Enumerable.Repeat(content, count)));
}
A.CallTo(() => schema.ScriptQuery)
.Returns(script);
private void SetupContents(Status[] status, int total, List<Guid> ids)
{
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<HashSet<Guid>>.Ignored))
.Returns(ResultList.Create(total, ids.Select(x => CreateContent(x)).Shuffle()));
}
foreach (var id in contentId)
private void SetupSchema()
{
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == id && x.Data == contentData), script))
.Returns(contentTransformed);
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema);
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaName))
.Returns(schema);
}
private void SetupSchemaNotFound()
{
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaName))
.Returns((ISchemaEntity)null);
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns((ISchemaEntity)null);
}
private IContentEntity CreateContent(Guid id, Status status = Status.Published)
@ -308,6 +428,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => content.Data).Returns(contentData);
A.CallTo(() => content.DataDraft).Returns(contentData);
A.CallTo(() => content.Status).Returns(status);
A.CallTo(() => content.SchemaId).Returns(new NamedId<Guid>(schemaId, schemaName));
return content;
}

8
tools/Migrate_01/MigrationPath.cs

@ -16,7 +16,7 @@ namespace Migrate_01
{
public sealed class MigrationPath : IMigrationPath
{
private const int CurrentVersion = 11;
private const int CurrentVersion = 12;
private readonly IServiceProvider serviceProvider;
public MigrationPath(IServiceProvider serviceProvider)
@ -58,6 +58,12 @@ namespace Migrate_01
yield return serviceProvider.GetRequiredService<RebuildSnapshots>();
}
// Version 12: Introduce roles.
else if (version < 12)
{
yield return serviceProvider.GetRequiredService<RebuildApps>();
}
// Version 09: Grain indexes.
if (version < 9)
{

27
tools/Migrate_01/Migrations/RebuildApps.cs

@ -0,0 +1,27 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Infrastructure.Migrations;
namespace Migrate_01.Migrations
{
public sealed class RebuildApps : IMigration
{
private readonly Rebuilder rebuilder;
public RebuildApps(Rebuilder rebuilder)
{
this.rebuilder = rebuilder;
}
public Task UpdateAsync()
{
return rebuilder.RebuildAppsAsync();
}
}
}
Loading…
Cancel
Save