diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index e6f3e6530..342c3cd71 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -99,6 +99,7 @@ "assets.metadata": "Metadata", "assets.metadataAdd": "Add Metadata", "assets.move": "Move", + "assets.moved": "Asset has been moved.", "assets.moveFailed": "Failed to move asset. Please reload.", "assets.protected": "Protected", "assets.protectedHint": "Assets are public by default. Everybody with the link can download the file. If you make an asset protected, only authenticated users (usually a client) can download the asset.", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 7bc159889..37edffb5a 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -99,6 +99,7 @@ "assets.metadata": "Metadati", "assets.metadataAdd": "Aggiungi un metadato", "assets.move": "Move", + "assets.moved": "Asset has been moved.", "assets.moveFailed": "Non è stato possibile spostare la risorsa. Per favore ricarica.", "assets.protected": "Protetto", "assets.protectedHint": "Assets are public by default. Everybody with the link can download the file. If you make an asset protected, only authenticated users (usually a client) can download the asset.", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index e4fc9ecdb..112673f52 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -99,6 +99,7 @@ "assets.metadata": "Metadata", "assets.metadataAdd": "Metadata toevoegen", "assets.move": "Move", + "assets.moved": "Asset has been moved.", "assets.moveFailed": "Verplaatsen van item is mislukt. Laad opnieuw.", "assets.protected": "Beschermd", "assets.protectedHint": "Assets are public by default. Everybody with the link can download the file. If you make an asset protected, only authenticated users (usually a client) can download the asset.", diff --git a/backend/i18n/frontend_pt.json b/backend/i18n/frontend_pt.json index 129858483..8a691c0e8 100644 --- a/backend/i18n/frontend_pt.json +++ b/backend/i18n/frontend_pt.json @@ -99,6 +99,7 @@ "assets.metadata": "Metadados", "assets.metadataAdd": "Adicionar metadados", "assets.move": "Move", + "assets.moved": "Asset has been moved.", "assets.moveFailed": "Falha ao mover ficheiro. Por favor, recarregue.", "assets.protected": "Protegido", "assets.protectedHint": "Os ativos são públicos por defeito. Todos com o link podem descarregar o ficheiro. Se fizer um ficheiro protegido, apenas utilizadores autenticados (normalmente um cliente) podem descarregar o ficheiro.", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index 29b6b4bd9..3a78f6b1e 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -99,6 +99,7 @@ "assets.metadata": "元数据", "assets.metadataAdd": "添加元数据", "assets.move": "Move", + "assets.moved": "Asset has been moved.", "assets.moveFailed": "资源移动失败。请重新加载。", "assets.protected": "受保护", "assets.protectedHint": "Assets are public by default. Everybody with the link can download the file. If you make an asset protected, only authenticated users (usually a client) can download the asset.", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index e6f3e6530..342c3cd71 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -99,6 +99,7 @@ "assets.metadata": "Metadata", "assets.metadataAdd": "Add Metadata", "assets.move": "Move", + "assets.moved": "Asset has been moved.", "assets.moveFailed": "Failed to move asset. Please reload.", "assets.protected": "Protected", "assets.protectedHint": "Assets are public by default. Everybody with the link can download the file. If you make an asset protected, only authenticated users (usually a client) can download the asset.", diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs index 4ae7775ba..fa5475f32 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/Guards/GuardApp.cs @@ -7,10 +7,14 @@ using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Billing; +using Squidex.Domain.Apps.Entities.Teams; using Squidex.Infrastructure; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; +using Squidex.Shared; +using Squidex.Shared.Identity; using Squidex.Text; +using System.Security.Claims; namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; @@ -136,7 +140,7 @@ public static class GuardApp var team = await appProvider.GetTeamAsync(command.TeamId.Value, ct); - if (team == null || !team.Contributors.ContainsKey(command.Actor.Identifier)) + if (team == null || (!IsContributor(team, command.Actor) && !HasTransferPermission(command.User))) { e(T.Get("apps.transfer.teamNotFound")); } @@ -146,6 +150,23 @@ public static class GuardApp e(T.Get("apps.transfer.planAssigned")); } }); + + static bool IsContributor(ITeamEntity team, RefToken actor) + { + return team.Contributors.ContainsKey(actor.Identifier); + } + + static bool HasTransferPermission(ClaimsPrincipal? user) + { + var permissions = user?.Claims.Permissions(); + + if (permissions == null) + { + return false; + } + + return permissions.Allows(PermissionIds.Transfer); + } } public static void CanChangePlan(ChangePlan command, IAppEntity app, IBillingPlans billingPlans) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs index 8df0edf56..35d38d108 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/SecurityExtensions.cs @@ -30,9 +30,7 @@ public static class SecurityExtensions throw new DomainForbiddenException(T.Get("common.errorNoPermission")); } - var permission = PermissionIds.ForApp(permissionId, context.App.Name, context.Schema.SchemaDef.Name); - - if (!permissions.Allows(permission)) + if (!permissions.Allows(permissionId, context.App.Name, context.Schema.SchemaDef.Name)) { throw new DomainForbiddenException(T.Get("common.errorNoPermission")); } diff --git a/backend/src/Squidex.Shared/PermissionIds.cs b/backend/src/Squidex.Shared/PermissionIds.cs index 64d3e4e96..bd77a214b 100644 --- a/backend/src/Squidex.Shared/PermissionIds.cs +++ b/backend/src/Squidex.Shared/PermissionIds.cs @@ -42,6 +42,9 @@ namespace Squidex.Shared // Team public const string Team = "squidex.teams.{team}"; + // Team Transfer + public const string Transfer = "squidex.transfer"; + // Team General public const string TeamAdmin = "squidex.teams.{team}.*"; public const string TeamUpdate = "squidex.teams.{team}.update"; diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index df2c37be5..d98684fc1 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -129,6 +129,7 @@ + diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs index 469fe7cd1..ac1fa46f2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.TestHelpers; @@ -47,6 +48,9 @@ public class AppDomainObjectTests : HandlerTestBase { user = UserMocks.User(contributorId); + A.CallTo(() => Team.Contributors) + .Returns(Contributors.Empty.Assign(User.Identifier, Role.Owner)); + A.CallTo(() => userResolver.FindByIdOrEmailAsync(contributorId, CancellationToken)) .Returns(user); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs index e28c6155f..7d8a4be02 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs @@ -14,6 +14,7 @@ using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Validation; +using Squidex.Shared; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards; @@ -22,7 +23,6 @@ public class GuardAppTests : GivenContext, IClassFixture { private readonly IUserResolver users = A.Fake(); private readonly IBillingPlans billingPlans = A.Fake(); - private readonly RefToken actor = RefToken.User("42"); public GuardAppTests() { @@ -34,6 +34,9 @@ public class GuardAppTests : GivenContext, IClassFixture A.CallTo(() => billingPlans.GetPlan("basic")) .Returns(new Plan()); + + A.CallTo(() => Team.Contributors) + .Returns(Contributors.Empty.Assign(User.Identifier, Role.Owner)); } [Fact] @@ -142,12 +145,22 @@ public class GuardAppTests : GivenContext, IClassFixture await GuardApp.CanTransfer(command, App, AppProvider, default); } + [Fact] + public async Task CanTransfer_should_not_throw_exception_if_user_has_transfer_permission() + { + var admin = Mocks.ApiUser(permission: PermissionIds.Transfer); + + var command = new TransferToTeam { TeamId = TeamId, Actor = User, User = admin }; + + await GuardApp.CanTransfer(command, App, AppProvider, default); + } + [Fact] public async Task CanTransfer_should_throw_exception_if_team_does_not_exist() { Team = null!; - var command = new TransferToTeam { TeamId = TeamId, Actor = actor }; + var command = new TransferToTeam { TeamId = TeamId, Actor = User }; await ValidationAssert.ThrowsAsync(() => GuardApp.CanTransfer(command, App, AppProvider, default), new ValidationError("The team does not exist.")); @@ -156,7 +169,9 @@ public class GuardAppTests : GivenContext, IClassFixture [Fact] public async Task CanTransfer_should_throw_exception_if_actor_is_not_part_of_team() { - var command = new TransferToTeam { TeamId = TeamId, Actor = actor }; + var nonContributor = RefToken.User("Other"); + + var command = new TransferToTeam { TeamId = TeamId, Actor = nonContributor }; await ValidationAssert.ThrowsAsync(() => GuardApp.CanTransfer(command, App, AppProvider, default), new ValidationError("The team does not exist.")); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs index 50c6867a5..66ece57fc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Scripting; @@ -38,7 +39,7 @@ public class DynamicContentWorkflowTests : GivenContext new Dictionary { [Status.Archived] = WorkflowTransition.Always, - [Status.Published] = WorkflowTransition.When("data.field.iv === 2", "Editor") + [Status.Published] = WorkflowTransition.When("data.field.iv === 2", Role.Editor) }.ToReadonlyDictionary(), StatusColors.Draft), [Status.Published] = @@ -48,7 +49,7 @@ public class DynamicContentWorkflowTests : GivenContext [Status.Archived] = WorkflowTransition.Always, [Status.Draft] = WorkflowTransition.Always }.ToReadonlyDictionary(), - StatusColors.Published, NoUpdate.When("data.field.iv === 2", "Owner", "Editor")) + StatusColors.Published, NoUpdate.When("data.field.iv === 2", Role.Owner, Role.Editor)) }.ToReadonlyDictionary()); public DynamicContentWorkflowTests() @@ -120,7 +121,7 @@ public class DynamicContentWorkflowTests : GivenContext [Fact] public async Task Should_allow_publish_on_create() { - var actual = await sut.CanPublishInitialAsync(Schema, Mocks.FrontendUser("Editor")); + var actual = await sut.CanPublishInitialAsync(Schema, Mocks.FrontendUser(Role.Editor)); Assert.True(actual); } @@ -128,7 +129,7 @@ public class DynamicContentWorkflowTests : GivenContext [Fact] public async Task Should_not_allow_publish_on_create_if_role_not_allowed() { - var actual = await sut.CanPublishInitialAsync(Schema, Mocks.FrontendUser("Developer")); + var actual = await sut.CanPublishInitialAsync(Schema, Mocks.FrontendUser(Role.Developer)); Assert.False(actual); } @@ -138,7 +139,7 @@ public class DynamicContentWorkflowTests : GivenContext { var content = CreateContent(Status.Draft, 2); - var actual = await sut.CanMoveToAsync(Schema, content.Status, Status.Published, content.Data, Mocks.FrontendUser("Editor")); + var actual = await sut.CanMoveToAsync(Schema, content.Status, Status.Published, content.Data, Mocks.FrontendUser(Role.Editor)); Assert.True(actual); } @@ -148,7 +149,7 @@ public class DynamicContentWorkflowTests : GivenContext { var content = CreateContent(Status.Draft, 2); - var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser(Role.Editor)); Assert.True(actual); } @@ -158,7 +159,7 @@ public class DynamicContentWorkflowTests : GivenContext { var content = CreateContent(Status.Draft, 2); - var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Developer")); + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser(Role.Developer)); Assert.False(actual); } @@ -168,7 +169,7 @@ public class DynamicContentWorkflowTests : GivenContext { var content = CreateContent(Status.Draft, 2); - var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser(Role.Editor)); Assert.True(actual); } @@ -178,7 +179,7 @@ public class DynamicContentWorkflowTests : GivenContext { var content = CreateContent(Status.Draft, 4); - var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser("Editor")); + var actual = await sut.CanMoveToAsync(content, content.Status, Status.Published, Mocks.FrontendUser(Role.Editor)); Assert.False(actual); } @@ -188,7 +189,7 @@ public class DynamicContentWorkflowTests : GivenContext { var content = CreateContent(Status.Published, 2); - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser(Role.Developer)); Assert.True(actual); } @@ -198,7 +199,7 @@ public class DynamicContentWorkflowTests : GivenContext { var content = CreateContent(Status.Published, 2); - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser(Role.Developer)); Assert.True(actual); } @@ -208,7 +209,7 @@ public class DynamicContentWorkflowTests : GivenContext { var content = CreateContent(Status.Archived, 2); - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Developer")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser(Role.Developer)); Assert.False(actual); } @@ -218,7 +219,7 @@ public class DynamicContentWorkflowTests : GivenContext { var content = CreateContent(Status.Published, 2); - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser(Role.Owner)); Assert.False(actual); } @@ -228,7 +229,7 @@ public class DynamicContentWorkflowTests : GivenContext { var content = CreateContent(Status.Published, 1); - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser(Role.Owner)); Assert.True(actual); } @@ -238,7 +239,7 @@ public class DynamicContentWorkflowTests : GivenContext { var content = CreateContent(Status.Published, 2); - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Editor")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser(Role.Editor)); Assert.False(actual); } @@ -248,7 +249,7 @@ public class DynamicContentWorkflowTests : GivenContext { var content = CreateContent(Status.Published, 1); - var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser("Owner")); + var actual = await sut.CanUpdateAsync(content, content.Status, Mocks.FrontendUser(Role.Owner)); Assert.True(actual); } @@ -263,7 +264,7 @@ public class DynamicContentWorkflowTests : GivenContext new StatusInfo(Status.Archived, StatusColors.Archived) }; - var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Developer")); + var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser(Role.Developer)); actual.Should().BeEquivalentTo(expected); } @@ -278,7 +279,7 @@ public class DynamicContentWorkflowTests : GivenContext new StatusInfo(Status.Archived, StatusColors.Archived) }; - var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Editor")); + var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser(Role.Editor)); actual.Should().BeEquivalentTo(expected); } @@ -294,7 +295,7 @@ public class DynamicContentWorkflowTests : GivenContext new StatusInfo(Status.Published, StatusColors.Published) }; - var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser("Editor")); + var actual = await sut.GetNextAsync(content, content.Status, Mocks.FrontendUser(Role.Editor)); actual.Should().BeEquivalentTo(expected); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/Indexes/TeamsIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/Indexes/TeamsIndexTests.cs index dcbdfb04a..6e1d6e3c8 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/Indexes/TeamsIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Teams/Indexes/TeamsIndexTests.cs @@ -75,7 +75,7 @@ public class TeamsIndexTests : GivenContext private static ITeamEntity SetupTeam(long version) { - var team = Mocks.Team(DomainId.NewGuid()); + var team = Mocks.Team(DomainId.NewGuid(), "my-team"); A.CallTo(() => team.Version) .Returns(version); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/GivenContext.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/GivenContext.cs index 4715fe381..bf29112fd 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/GivenContext.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/GivenContext.cs @@ -5,14 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Security.Claims; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Teams; using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Squidex.Shared; -using Squidex.Shared.Identity; namespace Squidex.Domain.Apps.Entities.TestHelpers; @@ -27,6 +23,8 @@ public abstract class GivenContext public DomainId TeamId { get; } = DomainId.NewGuid(); + public string TeamName { get; } = "my-team"; + public NamedId AppId { get; } = NamedId.Of(DomainId.NewGuid(), "my-app"); public NamedId SchemaId { get; } = NamedId.Of(DomainId.NewGuid(), "my-schema"); @@ -70,31 +68,27 @@ public abstract class GivenContext protected GivenContext() { - App = Mocks.App(AppId, Language.EN, Language.DE); + Team = Mocks.Team(TeamId, TeamName); - Team = Mocks.Team(TeamId, "my-team", User.Identifier); + App = Mocks.App(AppId, + Language.EN, + Language.DE); Schema = Mocks.Schema(AppId, SchemaId); } - public Context CreateContext(bool isFrontend, params string[] permissions) + public Context CreateContext(params string[] permissions) { - var claimsIdentity = new ClaimsIdentity(); - var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - - claimsIdentity.AddClaim(new Claim(OpenIdClaims.Subject, User.Identifier)); + var principal = Mocks.CreateUser(false, null, permissions); - if (isFrontend) - { - claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); - } + return new Context(principal, App); + } - foreach (var permission in permissions) - { - claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); - } + public Context CreateContext(bool isFrontend, params string[] permissions) + { + var principal = Mocks.CreateUser(isFrontend, null, permissions); - return new Context(claimsPrincipal, App); + return new Context(principal, App); } private static IContextProvider CreateContextProvider(Context context) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs index 51850694e..3e62b56c5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs @@ -54,43 +54,58 @@ public static class Mocks return schema; } - public static ITeamEntity Team(DomainId teamId, string teamName = "my-team", string contributor = "user") + public static ITeamEntity Team(DomainId teamId, string teamName) { var team = A.Fake(); A.CallTo(() => team.Id).Returns(teamId); A.CallTo(() => team.UniqueId).Returns(teamId); A.CallTo(() => team.Name).Returns(teamName); - A.CallTo(() => team.Contributors).Returns(Contributors.Empty.Assign(contributor, Role.Owner)); return team; } - public static ClaimsPrincipal ApiUser(string? role = null) + public static ClaimsPrincipal ApiUser(string? role = null, params string[] permissions) { - return CreateUser(role, "api"); + return CreateUser(false, role, permissions); + } + + public static ClaimsPrincipal ApiUser(string? role = null, string? permission = null) + { + return CreateUser(false, role, permission); + } + + public static ClaimsPrincipal FrontendUser(string? role = null, params string[] permissions) + { + return CreateUser(true, role, permissions); } public static ClaimsPrincipal FrontendUser(string? role = null, string? permission = null) { - return CreateUser(role, DefaultClients.Frontend, permission); + return CreateUser(true, role, permission); } - private static ClaimsPrincipal CreateUser(string? role, string client, string? permission = null) + public static ClaimsPrincipal CreateUser(bool isFrontend, string? role, params string?[] permissions) { var claimsIdentity = new ClaimsIdentity(); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); - claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, client)); + if (isFrontend) + { + claimsIdentity.AddClaim(new Claim(OpenIdClaims.ClientId, DefaultClients.Frontend)); + } if (role != null) { claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role)); } - if (permission != null) + foreach (var permission in permissions) { - claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); + if (permission != null) + { + claimsIdentity.AddClaim(new Claim(SquidexClaimTypes.Permissions, permission)); + } } return claimsPrincipal; diff --git a/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.html b/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.html index 800b733ff..660300e8e 100644 --- a/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.html +++ b/frontend/src/app/features/teams/pages/contributors/contributor-add-form.component.html @@ -24,7 +24,7 @@
- {{ 'contributors.importHintg' | sqxTranslate }} {{ 'contributors.importButton' | sqxTranslate }} + {{ 'contributors.importHint' | sqxTranslate }} {{ 'contributors.importButton' | sqxTranslate }}
diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index a5629de1d..5b02a7ac8 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -7,7 +7,7 @@ export const environment = { production: false, textLogger: true, textResolver: () => { - const culture = window['options']?.more?.culture || 'en'; + const culture = window['options']?.culture || 'en'; return require(`./../../../backend/i18n/frontend_${culture}.json`); }, diff --git a/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs b/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs index d55f0750d..163eada50 100644 --- a/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs @@ -1225,4 +1225,40 @@ public class ContentUpdateTests : IClassFixture } } } + + [Fact] + public async Task Should_create_content_with_custom_id_and_delete_it() + { + var id = "author-12345–fragment-text"; + + // STEP 1: Create a new item with a custom id. + var options = new ContentCreateOptions { Id = id, Publish = true }; + + var content = await _.Contents.CreateAsync(new TestEntityData + { + Number = 1 + }, options); + + Assert.Equal(id, content.Id); + + + // STEP 2: Delete with bulk update. + await _.Contents.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List + { + new BulkUpdateJob + { + Type = BulkUpdateType.Delete, + Id = id + } + } + }); + + + // STEP 3: Retrieve all items and ensure that the deleted item does not exist. + var updated = await _.Contents.GetAsync(); + + Assert.DoesNotContain(updated.Items, x => x.Id == id); + } }