Skip to content

Commit a0aff9d

Browse files
authored
Remove property value permissions when related content and/or property types are removed (#19778)
* Removed two unnecessary delete clauses when removing content types (they are looking for user group Ids, but we are deleting a content type). * Renamed table name constant with obsoletion to better reflect name and contents of table. * Added granular permission for property value records to delete clauses when deleting a document type. * Delete property value permissions for removed property types. * Added integration tests to verify behaviour.
1 parent 8a94383 commit a0aff9d

File tree

7 files changed

+124
-24
lines changed

7 files changed

+124
-24
lines changed

src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ ilist.[unique] AS isUnique
162162

163163
public override string ConvertDateToOrderableString => "{0}";
164164

165+
public override string ConvertUniqueIdentifierToString => "{0}";
166+
165167
public override string RenameTable => "ALTER TABLE {0} RENAME TO {1}";
166168

167169
/// <inheritdoc />

src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ public static class Tables
2020
public const string ContentType = /*TableNamePrefix*/ "cms" + "ContentType";
2121
public const string ContentChildType = /*TableNamePrefix*/ "cms" + "ContentTypeAllowedContentType";
2222
public const string DocumentType = /*TableNamePrefix*/ "cms" + "DocumentType";
23+
24+
[Obsolete("Please use ContentTypeTree instead. Scheduled for removal in Umbraco 18.")]
2325
public const string ElementTypeTree = /*TableNamePrefix*/ "cms" + "ContentType2ContentType";
26+
27+
public const string ContentTypeTree = /*TableNamePrefix*/ "cms" + "ContentType2ContentType";
28+
2429
public const string DataType = TableNamePrefix + "DataType";
2530
public const string Template = /*TableNamePrefix*/ "cms" + "Template";
2631

src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Umbraco.Cms.Infrastructure.Persistence.Dtos;
66

7-
[TableName(Constants.DatabaseSchema.Tables.ElementTypeTree)]
7+
[TableName(Constants.DatabaseSchema.Tables.ContentTypeTree)]
88
[ExplicitColumns]
99
internal sealed class ContentType2ContentTypeDto
1010
{

src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ protected void PersistUpdatedBaseContentType(IContentTypeComposition entity)
438438
IEnumerable<int> propertyTypeToDeleteIds = dbPropertyTypeIds.Except(entityPropertyTypes);
439439
foreach (var propertyTypeId in propertyTypeToDeleteIds)
440440
{
441-
DeletePropertyType(entity.Id, propertyTypeId);
441+
DeletePropertyType(entity, propertyTypeId);
442442
}
443443
}
444444

@@ -647,7 +647,7 @@ protected void PersistUpdatedBaseContentType(IContentTypeComposition entity)
647647
{
648648
foreach (var id in orphanPropertyTypeIds)
649649
{
650-
DeletePropertyType(entity.Id, id);
650+
DeletePropertyType(entity, id);
651651
}
652652
}
653653

@@ -1410,16 +1410,27 @@ private void RenormalizeDocumentEditedFlags(
14101410
}
14111411
}
14121412

1413-
private void DeletePropertyType(int contentTypeId, int propertyTypeId)
1413+
private void DeletePropertyType(IContentTypeComposition contentType, int propertyTypeId)
14141414
{
1415-
// first clear dependencies
1415+
// First clear dependencies.
14161416
Database.Delete<TagRelationshipDto>("WHERE propertyTypeId = @Id", new { Id = propertyTypeId });
14171417
Database.Delete<PropertyDataDto>("WHERE propertyTypeId = @Id", new { Id = propertyTypeId });
14181418

1419-
// then delete the property type
1419+
// Clear the property value permissions, which aren't a hard dependency with a foreign key, but we want to ensure
1420+
// that any for removed property types are cleared.
1421+
var uniqueIdAsString = string.Format(SqlContext.SqlSyntax.ConvertUniqueIdentifierToString, "uniqueId");
1422+
var permissionSearchString = SqlContext.SqlSyntax.GetConcat(
1423+
"(SELECT " + uniqueIdAsString + " FROM " + Constants.DatabaseSchema.Tables.PropertyType + " WHERE id = @PropertyTypeId)",
1424+
"'|%'");
1425+
1426+
Database.Delete<UserGroup2GranularPermissionDto>(
1427+
"WHERE uniqueId = @ContentTypeKey AND permission LIKE " + permissionSearchString,
1428+
new { ContentTypeKey = contentType.Key, PropertyTypeId = propertyTypeId });
1429+
1430+
// Finally delete the property type.
14201431
Database.Delete<PropertyTypeDto>(
14211432
"WHERE contentTypeId = @Id AND id = @PropertyTypeId",
1422-
new { Id = contentTypeId, PropertyTypeId = propertyTypeId });
1433+
new { contentType.Id, PropertyTypeId = propertyTypeId });
14231434
}
14241435

14251436
protected void ValidateAlias(TEntity entity)
@@ -1555,20 +1566,16 @@ protected override IEnumerable<string> GetDeleteClauses()
15551566
// is included here just to be 100% sure since it has a FK on cmsPropertyType.
15561567
var list = new List<string>
15571568
{
1558-
"DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id",
1559-
"DELETE FROM umbracoUserGroup2Permission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)",
1560-
"DELETE FROM umbracoUserGroup2GranularPermission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)",
1561-
"DELETE FROM cmsTagRelationship WHERE nodeId = @id",
1562-
"DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @id",
1563-
"DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @id",
1564-
"DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @id",
1565-
"DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @id",
1566-
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData +
1567-
" WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)",
1568-
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyType +
1569-
" WHERE contentTypeId = @id",
1570-
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyTypeGroup +
1571-
" WHERE contenttypeNodeId = @id",
1569+
"DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id",
1570+
"DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2GranularPermission + " WHERE uniqueId IN (SELECT uniqueId FROM umbracoNode WHERE id = @id)",
1571+
"DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id",
1572+
"DELETE FROM " + Constants.DatabaseSchema.Tables.ContentChildType + " WHERE Id = @id",
1573+
"DELETE FROM " + Constants.DatabaseSchema.Tables.ContentChildType + " WHERE AllowedId = @id",
1574+
"DELETE FROM " + Constants.DatabaseSchema.Tables.ContentTypeTree + " WHERE parentContentTypeId = @id",
1575+
"DELETE FROM " + Constants.DatabaseSchema.Tables.ContentTypeTree + " WHERE childContentTypeId = @id",
1576+
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)",
1577+
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyType + " WHERE contentTypeId = @id",
1578+
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyTypeGroup + " WHERE contenttypeNodeId = @id",
15721579
};
15731580
return list;
15741581
}

src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ public interface ISqlSyntaxProvider
7272

7373
string ConvertDecimalToOrderableString { get; }
7474

75+
string ConvertUniqueIdentifierToString => throw new NotImplementedException();
76+
7577
/// <summary>
7678
/// Returns the default isolation level for the database
7779
/// </summary>

src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,8 @@ public abstract void HandleCreateTable(
469469

470470
public virtual string ConvertDecimalToOrderableString => "REPLACE(STR({0}, 20, 9), SPACE(1), '0')";
471471

472+
public virtual string ConvertUniqueIdentifierToString => "CONVERT(nvarchar(36), {0})";
473+
472474
private DbTypes InitColumnTypeMap()
473475
{
474476
var dbTypeMap = new DbTypesFactory();

tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
// Copyright (c) Umbraco.
22
// See LICENSE for more details.
33

4-
using System.Collections.Generic;
5-
using System.Linq;
64
using Microsoft.Extensions.Logging;
75
using Microsoft.Extensions.Options;
86
using Moq;
97
using NUnit.Framework;
8+
using Umbraco.Cms.Api.Management.Mapping.Permissions;
109
using Umbraco.Cms.Core;
1110
using Umbraco.Cms.Core.Cache;
1211
using Umbraco.Cms.Core.Configuration.Models;
1312
using Umbraco.Cms.Core.IO;
1413
using Umbraco.Cms.Core.Mapping;
1514
using Umbraco.Cms.Core.Models;
16-
using Umbraco.Cms.Core.Models.ContentEditing;
15+
using Umbraco.Cms.Core.Models.Membership;
16+
using Umbraco.Cms.Core.Models.Membership.Permissions;
1717
using Umbraco.Cms.Core.Persistence;
1818
using Umbraco.Cms.Core.Persistence.Repositories;
1919
using Umbraco.Cms.Core.Services;
@@ -52,8 +52,11 @@ internal sealed class ContentTypeRepositoryTest : UmbracoIntegrationTest
5252
private IMediaTypeRepository MediaTypeRepository => GetRequiredService<IMediaTypeRepository>();
5353

5454
private IDocumentRepository DocumentRepository => GetRequiredService<IDocumentRepository>();
55+
5556
private IContentService ContentService => GetRequiredService<IContentService>();
5657

58+
private IUserGroupRepository UserGroupRepository => GetRequiredService<IUserGroupRepository>();
59+
5760
private ContentTypeRepository ContentTypeRepository =>
5861
(ContentTypeRepository)GetRequiredService<IContentTypeRepository>();
5962

@@ -918,4 +921,83 @@ public void Can_Update_Variation_Of_Element_Type_Property()
918921
Assert.That(hasCulture, Is.True);
919922
}
920923
}
924+
925+
[Test]
926+
public void Can_Remove_Property_Value_Permissions_On_Removal_Of_Property_Types()
927+
{
928+
var provider = ScopeProvider;
929+
using (var scope = provider.CreateScope())
930+
{
931+
// Create, save and re-retrieve a content type and user group.
932+
IContentType contentType = ContentTypeBuilder.CreateSimpleContentType(defaultTemplateId: 0);
933+
ContentTypeRepository.Save(contentType);
934+
contentType = ContentTypeRepository.Get(contentType.Id);
935+
936+
var userGroup = CreateUserGroupWithGranularPermissions(contentType);
937+
938+
// Remove property types and verify that the permission is removed from the user group.
939+
contentType.RemovePropertyType("author");
940+
ContentTypeRepository.Save(contentType);
941+
userGroup = UserGroupRepository.Get(userGroup.Id);
942+
Assert.AreEqual(3, userGroup.GranularPermissions.Count);
943+
944+
contentType.RemovePropertyType("bodyText");
945+
ContentTypeRepository.Save(contentType);
946+
userGroup = UserGroupRepository.Get(userGroup.Id);
947+
Assert.AreEqual(2, userGroup.GranularPermissions.Count);
948+
949+
contentType.RemovePropertyType("title");
950+
ContentTypeRepository.Save(contentType);
951+
userGroup = UserGroupRepository.Get(userGroup.Id);
952+
Assert.AreEqual(0, userGroup.GranularPermissions.Count);
953+
}
954+
}
955+
956+
[Test]
957+
public void Can_Remove_Property_Value_Permissions_On_Removal_Of_Content_Type()
958+
{
959+
var provider = ScopeProvider;
960+
using (var scope = provider.CreateScope())
961+
{
962+
// Create, save and re-retrieve a content type and user group.
963+
IContentType contentType = ContentTypeBuilder.CreateSimpleContentType(defaultTemplateId: 0);
964+
ContentTypeRepository.Save(contentType);
965+
contentType = ContentTypeRepository.Get(contentType.Id);
966+
967+
var userGroup = CreateUserGroupWithGranularPermissions(contentType);
968+
969+
// Remove the content type and verify all permissions are removed from the user group.
970+
ContentTypeRepository.Delete(contentType);
971+
userGroup = UserGroupRepository.Get(userGroup.Id);
972+
Assert.AreEqual(0, userGroup.GranularPermissions.Count);
973+
}
974+
}
975+
976+
private IUserGroup CreateUserGroupWithGranularPermissions(IContentType contentType)
977+
{
978+
DocumentPropertyValueGranularPermission CreatePermission(IPropertyType propertyType, string permission = "")
979+
=> new()
980+
{
981+
Key = contentType.Key,
982+
Permission = propertyType.Key.ToString().ToLowerInvariant() + "|" + permission,
983+
};
984+
985+
var titlePropertyType = contentType.PropertyTypes.Single(x => x.Alias == "title");
986+
var bodyTextPropertyType = contentType.PropertyTypes.Single(x => x.Alias == "bodyText");
987+
var authorPropertyType = contentType.PropertyTypes.Single(x => x.Alias == "author");
988+
989+
var userGroup = new UserGroupBuilder()
990+
.WithGranularPermissions([
991+
CreatePermission(titlePropertyType, "Umb.Document.PropertyValue.Read"),
992+
CreatePermission(titlePropertyType, "Umb.Document.PropertyValue.Write"),
993+
CreatePermission(bodyTextPropertyType, "Umb.Document.PropertyValue.Read"),
994+
CreatePermission(authorPropertyType)
995+
])
996+
.Build();
997+
UserGroupRepository.Save(userGroup);
998+
userGroup = UserGroupRepository.Get(userGroup.Id);
999+
1000+
Assert.AreEqual(4, userGroup.GranularPermissions.Count);
1001+
return userGroup;
1002+
}
9211003
}

0 commit comments

Comments
 (0)