Browse Source

Fix role validation for clients and contributors.

pull/332/head
Sebastian Stehle 7 years ago
parent
commit
73312a7686
  1. 4
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  2. 36
      src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  3. 4
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
  4. 4
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppContributors.cs
  5. 23
      src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs
  6. 5
      src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs
  7. 2
      src/Squidex/app/features/settings/pages/languages/language.component.html
  8. 14
      src/Squidex/app/features/settings/pages/roles/role.component.html
  9. 12
      src/Squidex/app/features/settings/pages/roles/role.component.scss
  10. 10
      src/Squidex/app/shared/services/app-roles.service.spec.ts
  11. 6
      src/Squidex/app/shared/services/app-roles.service.ts
  12. 6
      src/Squidex/app/shared/state/roles.state.spec.ts
  13. 2
      src/Squidex/app/shell/pages/internal/profile-menu.component.ts
  14. 15
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs
  15. 21
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppContributorsTests.cs

4
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -70,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
case AssignContributor assigneContributor:
return UpdateReturnAsync(assigneContributor, async c =>
{
await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId));
await GuardAppContributors.CanAssign(Snapshot.Contributors, c, userResolver, appPlansProvider.GetPlan(Snapshot.Plan?.PlanId), Snapshot.Roles);
AssignContributor(c);
@ -96,7 +96,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
case UpdateClient updateClient:
return UpdateAsync(updateClient, c =>
{
GuardAppClients.CanUpdate(Snapshot.Clients, c);
GuardAppClients.CanUpdate(Snapshot.Clients, c, Snapshot.Roles);
UpdateClient(c);
});

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

@ -63,6 +63,15 @@ namespace Squidex.Domain.Apps.Entities.Apps
AddEventMessage<AppPatternUpdated>(
"updated pattern {[Name]}");
AddEventMessage<AppRoleAdded>(
"added role {[Name]}");
AddEventMessage<AppRoleDeleted>(
"deleted role {[Name]}");
AddEventMessage<AppRoleUpdated>(
"updated role {[Name]}");
}
protected Task<HistoryEventToStore> On(AppContributorRemoved @event)
@ -173,6 +182,33 @@ namespace Squidex.Domain.Apps.Entities.Apps
.AddParameter("PatternId", @event.PatternId));
}
protected Task<HistoryEventToStore> On(AppRoleAdded @event)
{
const string channel = "settings.roles";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Name", @event.Name));
}
protected Task<HistoryEventToStore> On(AppRoleUpdated @event)
{
const string channel = "settings.roles";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Name", @event.Name));
}
protected Task<HistoryEventToStore> On(AppRoleDeleted @event)
{
const string channel = "settings.roles";
return Task.FromResult(
ForEvent(@event, channel)
.AddParameter("Name", @event.Name));
}
protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event)
{
return this.DispatchFuncAsync(@event.Payload, (HistoryEventToStore)null);

4
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs

@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
});
}
public static void CanUpdate(AppClients clients, UpdateClient command)
public static void CanUpdate(AppClients clients, UpdateClient command, Roles roles)
{
Guard.NotNull(command, nameof(command));
@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
e("Either name or role must be defined.", nameof(command.Name), nameof(command.Role));
}
if (command.Role != null && !Role.IsDefaultRole(command.Role))
if (command.Role != null && !roles.ContainsKey(command.Role))
{
e("Role is not valid.", nameof(command.Role));
}

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

@ -18,13 +18,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
public static class GuardAppContributors
{
public static Task CanAssign(AppContributors contributors, AssignContributor command, IUserResolver users, IAppLimitsPlan plan)
public static Task CanAssign(AppContributors contributors, AssignContributor command, IUserResolver users, IAppLimitsPlan plan, Roles roles)
{
Guard.NotNull(command, nameof(command));
return Validate.It(() => "Cannot assign contributor.", async e =>
{
if (!Role.IsDefaultRole(command.Role))
if (!roles.ContainsKey(command.Role))
{
e("Role is not valid.", nameof(command.Role));
}

23
src/Squidex/Areas/Api/Controllers/Apps/Models/RoleDto.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Apps;
@ -20,17 +21,33 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
[Required]
public string Name { get; set; }
/// <summary>
/// The number of clients with this role.
/// </summary>
public int NumClients { get; set; }
/// <summary>
/// The number of contributors with this role.
/// </summary>
public int NumContributors { get; set; }
/// <summary>
/// Associated list of permissions.
/// </summary>
[Required]
public string[] Permissions { get; set; }
public static RoleDto FromRole(Role role, string appName)
public static RoleDto FromRole(Role role, IAppEntity app)
{
var permissions = role.Permissions.WithoutApp(appName);
var permissions = role.Permissions.WithoutApp(app.Name);
return new RoleDto { Name = role.Name, Permissions = permissions.ToIds().ToArray() };
return new RoleDto
{
Name = role.Name,
NumClients = app.Clients.Count(x => string.Equals(x.Value.Role, role.Name, StringComparison.OrdinalIgnoreCase)),
NumContributors = app.Contributors.Count(x => string.Equals(x.Value, role.Name, StringComparison.OrdinalIgnoreCase)),
Permissions = permissions.ToIds().ToArray()
};
}
}
}

5
src/Squidex/Areas/Api/Controllers/Apps/Models/RolesDto.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Apps;
@ -15,14 +14,14 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
public sealed class RolesDto
{
/// <summary>
/// The roles.
/// The app roles.
/// </summary>
[Required]
public RoleDto[] Roles { get; set; }
public static RolesDto FromApp(IAppEntity app)
{
var roles = app.Roles.Values.Select(x => RoleDto.FromRole(x, app.Name)).ToArray();
var roles = app.Roles.Values.Select(x => RoleDto.FromRole(x, app)).ToArray();
return new RolesDto { Roles = roles };
}

2
src/Squidex/app/features/settings/pages/languages/language.component.html

@ -7,7 +7,7 @@
<div class="col col-6" [class.language-optional]="language.isOptional" [class.language-master]="language.isMaster">
{{language.englishName}}
</div>
<div class="col col-options">
<div class="col col-auto">
<div class="float-right">
<button type="button" class="btn btn-secondary table-items-edit-button" [class.active]="isEditing" (click)="toggleEditing()" *ngIf="!language.isMaster">
<i class="icon-settings"></i>

14
src/Squidex/app/features/settings/pages/roles/role.component.html

@ -1,16 +1,22 @@
<div class="table-items-row table-items-row-expandable">
<div class="table-items-row-summary">
<div class="row">
<div class="col" [class.built]="isDefaultRole">
{{role.name}}
<div class="col col-5" [class.built]="isDefaultRole">
<span class="role-name">{{role.name}}</span>
</div>
<div class="col col-options">
<div class="col text-decent">
Clients: <span [class.text-force]="role.numClients > 0">{{role.numClients}}</span>
</div>
<div class="col text-decent">
Users: <span [class.text-force]="role.numContributors > 0">{{role.numContributors}}</span>
</div>
<div class="col col-auto">
<div class="float-right">
<button type="button" class="btn btn-secondary table-items-edit-button" [class.active]="isEditing" (click)="toggleEditing()">
<i class="icon-settings"></i>
</button>
<button type="button" class="btn btn-link btn-danger" [class.invisible]="isDefaultRole"
<button type="button" class="btn btn-link btn-danger" [class.invisible]="isDefaultRole || role.numClients > 0 || role.numContributors > 0"
(sqxConfirmClick)="remove()"
confirmTitle="Delete role"
confirmText="Do you really want to delete the language?">

12
src/Squidex/app/features/settings/pages/roles/role.component.scss

@ -11,6 +11,18 @@
}
}
.rule-name {
@include truncate;
}
.text-decent {
color: $color-text-decent;
}
.text-force {
color: $color-text;
}
.built {
font-weight: bold;
}

10
src/Squidex/app/shared/services/app-roles.service.spec.ts

@ -75,9 +75,13 @@ describe('AppRolesService', () => {
req.flush({
roles: [{
name: 'Role1',
numClients: 3,
numContributors: 5,
permissions: ['P1']
}, {
name: 'Role2',
numClients: 7,
numContributors: 9,
permissions: ['P2']
}]
}, {
@ -88,8 +92,8 @@ describe('AppRolesService', () => {
expect(roles!).toEqual(
new AppRolesDto([
new AppRoleDto('Role1', ['P1']),
new AppRoleDto('Role2', ['P2'])
new AppRoleDto('Role1', 3, 5, ['P1']),
new AppRoleDto('Role2', 7, 9, ['P2'])
],
new Version('2')));
}));
@ -112,7 +116,7 @@ describe('AppRolesService', () => {
req.flush({});
expect(role!).toEqual(new AppRoleDto('Role3', []));
expect(role!).toEqual(new AppRoleDto('Role3', 0, 0, []));
}));
it('should make put request to update role',

6
src/Squidex/app/shared/services/app-roles.service.ts

@ -32,6 +32,8 @@ export class AppRolesDto extends Model {
export class AppRoleDto extends Model {
constructor(
public readonly name: string,
public readonly numClients: number,
public readonly numContributors: number,
public readonly permissions: string[]
) {
super();
@ -77,6 +79,8 @@ export class AppRolesService {
const roles = items.map(item => {
return new AppRoleDto(
item.name,
item.numClients,
item.numContributors,
item.permissions);
});
@ -90,7 +94,7 @@ export class AppRolesService {
return HTTP.postVersioned<any>(this.http, url, dto, version).pipe(
map(response => {
const role = new AppRoleDto(dto.name, []);
const role = new AppRoleDto(dto.name, 0, 0, []);
return new Versioned(response.version, role);
}),

6
src/Squidex/app/shared/state/roles.state.spec.ts

@ -26,8 +26,8 @@ describe('RolesState', () => {
const newVersion = new Version('2');
const oldRoles = [
new AppRoleDto('Role1', ['P1']),
new AppRoleDto('Role2', ['P2'])
new AppRoleDto('Role1', 3, 5, ['P1']),
new AppRoleDto('Role2', 7, 9, ['P2'])
];
let dialogs: IMock<DialogService>;
@ -69,7 +69,7 @@ describe('RolesState', () => {
});
it('should add role to snapshot when added', () => {
const newRole = new AppRoleDto('Role3', ['P3']);
const newRole = new AppRoleDto('Role3', 0, 0, ['P3']);
const request = new CreateAppRoleDto('Role3');

2
src/Squidex/app/shell/pages/internal/profile-menu.component.ts

@ -52,7 +52,7 @@ export class ProfileMenuComponent implements OnDestroy, OnInit {
.subscribe(user => {
this.profileId = user!.id;
this.profileDisplayName = user!.displayName;
this.changeDetector.markForCheck();
});
}

15
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs

@ -18,6 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
public class GuardAppClientsTests
{
private readonly AppClients clients_0 = AppClients.Empty;
private readonly Roles roles = Roles.CreateDefaults("my-app");
[Fact]
public void CanAttach_should_throw_execption_if_client_id_is_null()
@ -81,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new UpdateClient { Name = "iOS" };
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_0, command),
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_0, command, Roles.Empty),
new ValidationError("Client id is required.", "Id"));
}
@ -90,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new UpdateClient { Id = "ios", Name = "iOS" };
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppClients.CanUpdate(clients_0, command));
Assert.Throws<DomainObjectNotFoundException>(() => GuardAppClients.CanUpdate(clients_0, command, Roles.Empty));
}
[Fact]
@ -100,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var clients_1 = clients_0.Add("ios", "secret");
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command),
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command, roles),
new ValidationError("Either name or role must be defined.", "Name", "Role"));
}
@ -111,7 +112,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var clients_1 = clients_0.Add("ios", "secret");
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command),
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command, roles),
new ValidationError("Role is not valid.", "Role"));
}
@ -122,7 +123,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var clients_1 = clients_0.Add("ios", "secret");
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command),
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command, roles),
new ValidationError("Client has already this name.", "Name"));
}
@ -133,7 +134,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var clients_1 = clients_0.Add("ios", "secret");
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command),
ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command, roles),
new ValidationError("Client has already this role.", "Role"));
}
@ -144,7 +145,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var clients_1 = clients_0.Add("ios", "secret");
GuardAppClients.CanUpdate(clients_1, command);
GuardAppClients.CanUpdate(clients_1, command, roles);
}
}
}

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

@ -27,6 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
private readonly IUserResolver users = A.Fake<IUserResolver>();
private readonly IAppLimitsPlan appPlan = A.Fake<IAppLimitsPlan>();
private readonly AppContributors contributors_0 = AppContributors.Empty;
private readonly Roles roles = Roles.CreateDefaults("my-app");
public GuardAppContributorsTests()
{
@ -54,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new AssignContributor();
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan),
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan, roles),
new ValidationError("Contributor id is required.", "ContributorId"));
}
@ -63,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new AssignContributor { ContributorId = "1", Role = "Invalid" };
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan),
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan, roles),
new ValidationError("Role is not valid.", "Role"));
}
@ -74,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var contributors_1 = contributors_0.Assign("1", Role.Owner);
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, command, users, appPlan),
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_1, command, users, appPlan, roles),
new ValidationError("Contributor has already this role.", "Role"));
}
@ -83,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new AssignContributor { ContributorId = "notfound", Role = Role.Owner };
return Assert.ThrowsAsync<DomainObjectNotFoundException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan));
return Assert.ThrowsAsync<DomainObjectNotFoundException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan, roles));
}
[Fact]
@ -91,7 +92,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<DomainForbiddenException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan));
return Assert.ThrowsAsync<DomainForbiddenException>(() => GuardAppContributors.CanAssign(contributors_0, command, users, appPlan, roles));
}
[Fact]
@ -105,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var contributors_1 = contributors_0.Assign("1", Role.Owner);
var contributors_2 = contributors_1.Assign("2", Role.Editor);
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_2, command, users, appPlan),
return ValidationAssert.ThrowsAsync(() => GuardAppContributors.CanAssign(contributors_2, command, users, appPlan, roles),
new ValidationError("You have reached the maximum number of contributors for your plan."));
}
@ -114,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new AssignContributor { ContributorId = "1@email.com" };
await GuardAppContributors.CanAssign(contributors_0, command, users, appPlan);
await GuardAppContributors.CanAssign(contributors_0, command, users, appPlan, roles);
Assert.Equal("1", command.ContributorId);
}
@ -124,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
var command = new AssignContributor { ContributorId = "1" };
return GuardAppContributors.CanAssign(contributors_0, command, users, appPlan);
return GuardAppContributors.CanAssign(contributors_0, command, users, appPlan, roles);
}
[Fact]
@ -134,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var contributors_1 = contributors_0.Assign("1", Role.Editor);
return GuardAppContributors.CanAssign(contributors_1, command, users, appPlan);
return GuardAppContributors.CanAssign(contributors_1, command, users, appPlan, roles);
}
[Fact]
@ -148,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
var contributors_1 = contributors_0.Assign("1", Role.Editor);
var contributors_2 = contributors_1.Assign("2", Role.Editor);
return GuardAppContributors.CanAssign(contributors_2, command, users, appPlan);
return GuardAppContributors.CanAssign(contributors_2, command, users, appPlan, roles);
}
[Fact]

Loading…
Cancel
Save