Skip to content

Remove property value permissions when related content and/or property types are removed #19778

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ ilist.[unique] AS isUnique

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

public override string ConvertUniqueIdentifierToString => "{0}";

public override string RenameTable => "ALTER TABLE {0} RENAME TO {1}";

/// <inheritdoc />
Expand Down
5 changes: 5 additions & 0 deletions src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ public static class Tables
public const string ContentType = /*TableNamePrefix*/ "cms" + "ContentType";
public const string ContentChildType = /*TableNamePrefix*/ "cms" + "ContentTypeAllowedContentType";
public const string DocumentType = /*TableNamePrefix*/ "cms" + "DocumentType";

[Obsolete("Please use ContentTypeTree instead. Scheduled for removal in Umbraco 18.")]
public const string ElementTypeTree = /*TableNamePrefix*/ "cms" + "ContentType2ContentType";

public const string ContentTypeTree = /*TableNamePrefix*/ "cms" + "ContentType2ContentType";

public const string DataType = TableNamePrefix + "DataType";
public const string Template = /*TableNamePrefix*/ "cms" + "Template";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Umbraco.Cms.Infrastructure.Persistence.Dtos;

[TableName(Constants.DatabaseSchema.Tables.ElementTypeTree)]
[TableName(Constants.DatabaseSchema.Tables.ContentTypeTree)]
[ExplicitColumns]
internal sealed class ContentType2ContentTypeDto
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Data;

Check notice on line 1 in src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

✅ No longer an issue: Primitive Obsession

The ratio of primivite types in function arguments is no longer above the threshold
using System.Globalization;
using System.Linq.Expressions;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -438,7 +438,7 @@
IEnumerable<int> propertyTypeToDeleteIds = dbPropertyTypeIds.Except(entityPropertyTypes);
foreach (var propertyTypeId in propertyTypeToDeleteIds)
{
DeletePropertyType(entity.Id, propertyTypeId);
DeletePropertyType(entity, propertyTypeId);
}
}

Expand Down Expand Up @@ -647,7 +647,7 @@
{
foreach (var id in orphanPropertyTypeIds)
{
DeletePropertyType(entity.Id, id);
DeletePropertyType(entity, id);
}
}

Expand Down Expand Up @@ -1410,16 +1410,27 @@
}
}

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

// then delete the property type
// Clear the property value permissions, which aren't a hard dependency with a foreign key, but we want to ensure
// that any for removed property types are cleared.
var uniqueIdAsString = string.Format(SqlContext.SqlSyntax.ConvertUniqueIdentifierToString, "uniqueId");
var permissionSearchString = SqlContext.SqlSyntax.GetConcat(
"(SELECT " + uniqueIdAsString + " FROM " + Constants.DatabaseSchema.Tables.PropertyType + " WHERE id = @PropertyTypeId)",
"'|%'");

Database.Delete<UserGroup2GranularPermissionDto>(
"WHERE uniqueId = @ContentTypeKey AND permission LIKE " + permissionSearchString,
new { ContentTypeKey = contentType.Key, PropertyTypeId = propertyTypeId });

// Finally delete the property type.
Database.Delete<PropertyTypeDto>(
"WHERE contentTypeId = @Id AND id = @PropertyTypeId",
new { Id = contentTypeId, PropertyTypeId = propertyTypeId });
new { contentType.Id, PropertyTypeId = propertyTypeId });
}

protected void ValidateAlias(TEntity entity)
Expand Down Expand Up @@ -1555,20 +1566,16 @@
// is included here just to be 100% sure since it has a FK on cmsPropertyType.
var list = new List<string>
{
"DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id",
"DELETE FROM umbracoUserGroup2Permission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)",
"DELETE FROM umbracoUserGroup2GranularPermission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)",
"DELETE FROM cmsTagRelationship WHERE nodeId = @id",
"DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @id",
"DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @id",
"DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @id",
"DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData +
" WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)",
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyType +
" WHERE contentTypeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyTypeGroup +
" WHERE contenttypeNodeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2GranularPermission + " WHERE uniqueId IN (SELECT uniqueId FROM umbracoNode WHERE id = @id)",
"DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.ContentChildType + " WHERE Id = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.ContentChildType + " WHERE AllowedId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.ContentTypeTree + " WHERE parentContentTypeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.ContentTypeTree + " WHERE childContentTypeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)",
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyType + " WHERE contentTypeId = @id",
"DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyTypeGroup + " WHERE contenttypeNodeId = @id",
};
return list;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public interface ISqlSyntaxProvider

string ConvertDecimalToOrderableString { get; }

string ConvertUniqueIdentifierToString => throw new NotImplementedException();

/// <summary>
/// Returns the default isolation level for the database
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,8 @@ public abstract void HandleCreateTable(

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

public virtual string ConvertUniqueIdentifierToString => "CONVERT(nvarchar(36), {0})";

private DbTypes InitColumnTypeMap()
{
var dbTypeMap = new DbTypesFactory();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.

using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Api.Management.Mapping.Permissions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.Membership.Permissions;
using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Services;
Expand Down Expand Up @@ -52,8 +52,11 @@ internal sealed class ContentTypeRepositoryTest : UmbracoIntegrationTest
private IMediaTypeRepository MediaTypeRepository => GetRequiredService<IMediaTypeRepository>();

private IDocumentRepository DocumentRepository => GetRequiredService<IDocumentRepository>();

private IContentService ContentService => GetRequiredService<IContentService>();

private IUserGroupRepository UserGroupRepository => GetRequiredService<IUserGroupRepository>();

private ContentTypeRepository ContentTypeRepository =>
(ContentTypeRepository)GetRequiredService<IContentTypeRepository>();

Expand Down Expand Up @@ -918,4 +921,83 @@ public void Can_Update_Variation_Of_Element_Type_Property()
Assert.That(hasCulture, Is.True);
}
}

[Test]
public void Can_Remove_Property_Value_Permissions_On_Removal_Of_Property_Types()
{
var provider = ScopeProvider;
using (var scope = provider.CreateScope())
{
// Create, save and re-retrieve a content type and user group.
IContentType contentType = ContentTypeBuilder.CreateSimpleContentType(defaultTemplateId: 0);
ContentTypeRepository.Save(contentType);
contentType = ContentTypeRepository.Get(contentType.Id);

var userGroup = CreateUserGroupWithGranularPermissions(contentType);

// Remove property types and verify that the permission is removed from the user group.
contentType.RemovePropertyType("author");
ContentTypeRepository.Save(contentType);
userGroup = UserGroupRepository.Get(userGroup.Id);
Assert.AreEqual(3, userGroup.GranularPermissions.Count);

contentType.RemovePropertyType("bodyText");
ContentTypeRepository.Save(contentType);
userGroup = UserGroupRepository.Get(userGroup.Id);
Assert.AreEqual(2, userGroup.GranularPermissions.Count);

contentType.RemovePropertyType("title");
ContentTypeRepository.Save(contentType);
userGroup = UserGroupRepository.Get(userGroup.Id);
Assert.AreEqual(0, userGroup.GranularPermissions.Count);
}
}

[Test]
public void Can_Remove_Property_Value_Permissions_On_Removal_Of_Content_Type()
{
var provider = ScopeProvider;
using (var scope = provider.CreateScope())
{
// Create, save and re-retrieve a content type and user group.
IContentType contentType = ContentTypeBuilder.CreateSimpleContentType(defaultTemplateId: 0);
ContentTypeRepository.Save(contentType);
contentType = ContentTypeRepository.Get(contentType.Id);

var userGroup = CreateUserGroupWithGranularPermissions(contentType);

// Remove the content type and verify all permissions are removed from the user group.
ContentTypeRepository.Delete(contentType);
userGroup = UserGroupRepository.Get(userGroup.Id);
Assert.AreEqual(0, userGroup.GranularPermissions.Count);
}
}

private IUserGroup CreateUserGroupWithGranularPermissions(IContentType contentType)
{
DocumentPropertyValueGranularPermission CreatePermission(IPropertyType propertyType, string permission = "")
=> new()
{
Key = contentType.Key,
Permission = propertyType.Key.ToString().ToLowerInvariant() + "|" + permission,
};

var titlePropertyType = contentType.PropertyTypes.Single(x => x.Alias == "title");
var bodyTextPropertyType = contentType.PropertyTypes.Single(x => x.Alias == "bodyText");
var authorPropertyType = contentType.PropertyTypes.Single(x => x.Alias == "author");

var userGroup = new UserGroupBuilder()
.WithGranularPermissions([
CreatePermission(titlePropertyType, "Umb.Document.PropertyValue.Read"),
CreatePermission(titlePropertyType, "Umb.Document.PropertyValue.Write"),
CreatePermission(bodyTextPropertyType, "Umb.Document.PropertyValue.Read"),
CreatePermission(authorPropertyType)
])
.Build();
UserGroupRepository.Save(userGroup);
userGroup = UserGroupRepository.Get(userGroup.Id);

Assert.AreEqual(4, userGroup.GranularPermissions.Count);
return userGroup;
}
}
2 changes: 1 addition & 1 deletion tests/Umbraco.Tests.Integration/appsettings.Tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"Tests": {
"Database": {
"DatabaseType": "SQLite",
"DatabaseType": "SQLite", // "SQLite", "LocalDb"
"PrepareThreadCount": 4,
"SchemaDatabaseCount": 4,
"EmptyDatabasesCount": 2,
Expand Down