Skip to content
Closed
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 @@ -96,4 +96,46 @@ public interface AbstractRolePermissionView {
)
@Nullable
Map<PermissionAPI.Scope, Set<PermissionAPI.Type>> inheritable();

/**
* Gets the type of the asset from which permissions are inherited.
* Only populated when {@link #inherited()} is true.
*
* @return Inherited source type (host, folder, category, structure), or empty string if not inherited
*/
@JsonProperty("inheritedFromType")
@Schema(
description = "Type of the parent asset from which permissions are inherited (host, folder, category, structure)",
example = "folder"
)
@Value.Default
default String inheritedFromType() { return ""; }

/**
* Gets the display path/name of the asset from which permissions are inherited.
* Only populated when {@link #inherited()} is true.
*
* @return Inherited source path or name, or empty string if not inherited
*/
@JsonProperty("inheritedFromPath")
@Schema(
description = "Display path or name of the parent asset from which permissions are inherited",
example = "/about-us/"
)
@Value.Default
default String inheritedFromPath() { return ""; }

/**
* Gets the ID of the asset from which permissions are inherited.
* Only populated when {@link #inherited()} is true.
*
* @return Inherited source asset ID, or empty string if not inherited
*/
@JsonProperty("inheritedFromId")
@Schema(
description = "Identifier of the parent asset from which permissions are inherited",
example = "abc-123-def-456"
)
@Value.Default
default String inheritedFromId() { return ""; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.dotmarketing.business.RoleAPI;
import com.dotmarketing.exception.DotDataException;
import com.dotmarketing.exception.DotSecurityException;
import com.dotmarketing.portlets.categories.model.Category;
import com.dotmarketing.portlets.containers.model.Container;
import com.dotmarketing.portlets.contentlet.business.HostAPI;
import com.dotmarketing.portlets.contentlet.model.Contentlet;
Expand Down Expand Up @@ -159,6 +160,18 @@ public Permissionable resolveAsset(final String assetId)
Logger.debug(this, () -> String.format("Not a container: %s", assetId));
}

// Try Category
try {
final Category category = APILocator.getCategoryAPI()
.find(assetId, systemUser, respectFrontendRoles);
if (category != null && UtilMethods.isSet(category.getInode())) {
Logger.debug(this, () -> String.format("Resolved as Category: %s", assetId));
return category;
}
} catch (Exception e) {
Logger.debug(this, () -> String.format("Not a category: %s", assetId));
}

Logger.warn(this, String.format("Unable to resolve asset: %s", assetId));
return null;
}
Expand Down Expand Up @@ -227,6 +240,26 @@ public List<RolePermissionView> buildRolePermissions(final Permissionable asset,
final boolean isInheriting = permissionAPI.isInheritingPermissions(asset);
final boolean isParentPermissionable = asset.isParentPermissionable();

// Resolve the parent permissionable for inherited-from info
String inheritedFromType = "";
String inheritedFromPath = "";
String inheritedFromId = "";
if (isInheriting) {
try {
final Permissionable parentPerm = permissionAPI.findParentPermissionable(asset);
if (parentPerm != null) {
final String[] inheritInfo = resolveInheritedFromInfo(parentPerm);
inheritedFromType = inheritInfo[0];
inheritedFromPath = inheritInfo[1];
inheritedFromId = inheritInfo[2];
}
} catch (Exception e) {
Logger.debug(this, () -> String.format(
"Could not resolve parent permissionable for asset: %s - %s",
asset.getPermissionId(), e.getMessage()));
}
}

// Group permissions by role ID
final Map<String, List<Permission>> permissionsByRole = permissions.stream()
.collect(Collectors.groupingBy(Permission::getRoleId, LinkedHashMap::new, Collectors.toList()));
Expand Down Expand Up @@ -256,9 +289,13 @@ public List<RolePermissionView> buildRolePermissions(final Permissionable asset,
// Build individual permissions set
final Set<PermissionAPI.Type> individual = convertPermissionsToTypeSet(individualPermissions);

// Build inheritable permissions map (only for parent permissionables)
// Build inheritable permissions map.
// Always include when there are inheritable-type permissions, even for
// non-parent permissionables. The DWR compatibility layer in the JSP needs
// these to merge into the "individual" bucket (matching legacy DWR behavior
// where getPermissions() forced all types to "individual").
final Map<PermissionAPI.Scope, Set<PermissionAPI.Type>> inheritable;
if (isParentPermissionable && !inheritablePermissions.isEmpty()) {
if (!inheritablePermissions.isEmpty()) {
inheritable = buildInheritablePermissionMap(inheritablePermissions);
} else {
inheritable = null;
Expand All @@ -268,6 +305,9 @@ public List<RolePermissionView> buildRolePermissions(final Permissionable asset,
.roleId(roleId)
.roleName(role.getName())
.inherited(isInheriting)
.inheritedFromType(inheritedFromType)
.inheritedFromPath(inheritedFromPath)
.inheritedFromId(inheritedFromId)
.individual(individual)
.inheritable(inheritable)
.build();
Expand All @@ -283,6 +323,57 @@ public List<RolePermissionView> buildRolePermissions(final Permissionable asset,
return rolePermissions;
}

/**
* Resolves the inherited-from metadata (type, path, id) for a parent permissionable.
* Matches the legacy DWR PermissionAjax behavior for the "Getting permissions from parent:" display.
*
* @param parentPerm The parent permissionable
* @return String array [type, path, id]
*/
private String[] resolveInheritedFromInfo(final Permissionable parentPerm) {
try {
if (parentPerm instanceof Folder) {
final Folder folder = (Folder) parentPerm;
final String path = APILocator.getIdentifierAPI().find(folder.getIdentifier()).getPath();
return new String[]{"folder", path, folder.getInode()};
} else if (parentPerm instanceof Host) {
final Host host = (Host) parentPerm;
return new String[]{"host", host.getHostname(), host.getIdentifier()};
} else if (parentPerm instanceof Contentlet && ((Contentlet) parentPerm).isHost()) {
final Host host = new Host((Contentlet) parentPerm);
return new String[]{"host", host.getHostname(), host.getIdentifier()};
} else if (parentPerm instanceof Category) {
final Category category = (Category) parentPerm;
return new String[]{"category", category.getCategoryName(), category.getInode()};
} else if (parentPerm instanceof com.dotcms.contenttype.model.type.ContentType) {
final com.dotcms.contenttype.model.type.ContentType ct =
(com.dotcms.contenttype.model.type.ContentType) parentPerm;
return new String[]{"structure", ct.name(), ct.inode()};
} else if (parentPerm instanceof com.dotmarketing.portlets.structure.model.Structure) {
final com.dotmarketing.portlets.structure.model.Structure structure =
(com.dotmarketing.portlets.structure.model.Structure) parentPerm;
return new String[]{"structure", structure.getName(), structure.getInode()};
}
} catch (Exception e) {
Logger.debug(this, () -> String.format(
"Error resolving inherited-from info for permissionable: %s - %s",
parentPerm.getPermissionId(), e.getMessage()));
}

// Fallback: try to resolve as host by permission ID
try {
final Host host = hostAPI.find(parentPerm.getPermissionId(),
APILocator.getUserAPI().getSystemUser(), false);
if (host != null && UtilMethods.isSet(host.getIdentifier())) {
return new String[]{"host", host.getHostname(), host.getIdentifier()};
}
} catch (Exception e) {
Logger.debug(this, () -> "Fallback host lookup failed");
}

return new String[]{"", "", ""};
}

/**
* Converts a list of permissions to a set of permission types.
* Handles bit-packed permissions correctly using EnumSet for efficiency.
Expand Down Expand Up @@ -990,6 +1081,51 @@ private UpdateRolePermissionsView buildRolePermissionUpdateResponse(final Permis
.build();
}

/**
* Builds a PermissionableObjectView for the given asset.
* Returns metadata needed by the permissions tab UI to determine which
* permission options to display.
*
* <p>Replaces the legacy {@code PermissionAjax.getAsset()} DWR method.
*
* @param assetId Asset identifier (inode or identifier)
* @param user Requesting user
* @return PermissionableObjectView containing asset metadata
* @throws DotDataException If there's an error accessing data
* @throws DotSecurityException If security validation fails
*/
public PermissionableObjectView getPermissionableObjectView(final String assetId,
final User user)
throws DotDataException, DotSecurityException {

final Permissionable asset = resolveAsset(assetId);
if (asset == null) {
throw new NotFoundInDbException(String.format("Asset not found: %s", assetId));
}

final boolean isFolder = asset instanceof Folder;
final boolean isHost = asset instanceof Host
|| (asset instanceof Contentlet
&& ((Contentlet) asset).isHost());
final boolean isContentType = asset instanceof com.dotcms.contenttype.model.type.ContentType
|| asset instanceof com.dotmarketing.portlets.structure.model.Structure;
final boolean isParentPermissionable = asset.isParentPermissionable();
final boolean canEditPermissions = permissionAPI.doesUserHavePermission(
asset, PermissionAPI.PERMISSION_EDIT_PERMISSIONS, user, false);

final String type = asset.getClass().getName();

return new PermissionableObjectView(
asset.getPermissionId(),
type,
isFolder,
isHost,
isContentType,
isParentPermissionable,
canEditPermissions
);
}

/**
* Builds asset view with permissions filtered to a specific role.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.dotcms.rest.InitDataObject;
import com.dotcms.rest.Pagination;
import com.dotcms.rest.ResponseEntityPaginatedDataView;
import com.dotcms.rest.ResponseEntityStringView;
import com.dotcms.rest.ResponseEntityView;
import com.dotcms.rest.WebResource;
import com.dotcms.rest.annotation.NoCache;
Expand Down Expand Up @@ -1142,4 +1143,148 @@ public ResponseEntityUpdateRolePermissionsView updateRolePermissions(

return new ResponseEntityUpdateRolePermissionsView(result);
}

/**
* Breaks permission inheritance for a specific asset, making it have its own
* individual permissions (copied from parent). This is used when an asset is
* currently inheriting permissions and the user wants to customize them.
*
* @param request HTTP servlet request
* @param response HTTP servlet response
* @param assetId Asset identifier (inode or identifier)
* @return ResponseEntityStringView with success message
* @throws DotDataException If there's an error accessing permission data
* @throws DotSecurityException If security validation fails
*/
@Operation(
summary = "Break permission inheritance",
description = "Breaks permission inheritance for a specific asset, giving it individual " +
"permissions copied from its parent. After this operation, the asset's permissions " +
"can be modified independently of the parent."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "Inheritance broken successfully",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ResponseEntityStringView.class))),
@ApiResponse(responseCode = "401",
description = "Unauthorized - authentication required",
content = @Content(mediaType = "application/json")),
@ApiResponse(responseCode = "403",
description = "Forbidden - user lacks EDIT_PERMISSIONS on asset",
content = @Content(mediaType = "application/json")),
@ApiResponse(responseCode = "404",
description = "Asset not found",
content = @Content(mediaType = "application/json"))
})
@PUT
@Path("/{assetId}/_individualpermission")
@JSONP
@NoCache
@Produces({MediaType.APPLICATION_JSON})
public ResponseEntityStringView permissionIndividually(
final @Context HttpServletRequest request,
final @Context HttpServletResponse response,
@Parameter(description = "Asset identifier (inode or identifier)", required = true)
final @PathParam("assetId") String assetId)
throws DotDataException, DotSecurityException {

Logger.debug(this, () -> String.format(
"permissionIndividually called - assetId: %s", assetId));

final User user = new WebResource.InitBuilder(webResource)
.requiredBackendUser(true)
.requiredFrontendUser(false)
.requestAndResponse(request, response)
.rejectWhenNoUser(true)
.init()
.getUser();

if (!UtilMethods.isSet(assetId)) {
throw new BadRequestException("Asset ID is required");
}

final Permissionable asset = assetPermissionHelper.resolveAsset(assetId);
if (asset == null) {
throw new NotFoundInDbException(String.format("Asset not found: %s", assetId));
}

final PermissionAPI permissionAPI = APILocator.getPermissionAPI();
if (!permissionAPI.doesUserHavePermission(asset, PermissionAPI.PERMISSION_EDIT_PERMISSIONS, user)) {
throw new DotSecurityException("User does not have permission to edit permissions on this asset");
}

final Permissionable parentPermissionable = permissionAPI.findParentPermissionable(asset);
if (parentPermissionable != null) {
permissionAPI.permissionIndividually(parentPermissionable, asset, user);
}

Logger.info(this, () -> String.format(
"Successfully broke permission inheritance for asset: %s", assetId));

return new ResponseEntityStringView("Permission inheritance broken successfully");
}

/**
* Retrieves metadata about a permissionable asset, including type information
* and permission flags. Used by the permissions tab UI to determine which
* permission options to display.
*
* @param request HTTP servlet request
* @param response HTTP servlet response
* @param assetId Asset identifier (inode or identifier)
* @return ResponseEntityPermissionableObjectView containing asset metadata
* @throws DotDataException If there's an error accessing data
* @throws DotSecurityException If security validation fails
*/
@Operation(
summary = "Get permissionable asset metadata",
description = "Retrieves metadata about a permissionable asset including its type, " +
"whether it is a folder/host/content type, and whether the current user " +
"has permission to edit its permissions."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200",
description = "Asset metadata retrieved successfully",
content = @Content(mediaType = "application/json",
schema = @Schema(implementation = ResponseEntityPermissionableObjectView.class))),
@ApiResponse(responseCode = "401",
description = "Unauthorized - authentication required",
content = @Content(mediaType = "application/json")),
@ApiResponse(responseCode = "404",
description = "Asset not found",
content = @Content(mediaType = "application/json"))
})
@GET
@Path("/{assetId}/_permissionable")
@JSONP
@NoCache
@Produces({MediaType.APPLICATION_JSON})
public ResponseEntityPermissionableObjectView getPermissionableObject(
final @Context HttpServletRequest request,
final @Context HttpServletResponse response,
@Parameter(description = "Asset identifier (inode or identifier)", required = true)
final @PathParam("assetId") String assetId)
throws DotDataException, DotSecurityException {

Logger.debug(this, () -> String.format(
"getPermissionableObject called - assetId: %s", assetId));

final User user = new WebResource.InitBuilder(webResource)
.requiredBackendUser(true)
.requiredFrontendUser(false)
.requestAndResponse(request, response)
.rejectWhenNoUser(true)
.init()
.getUser();

if (!UtilMethods.isSet(assetId)) {
throw new BadRequestException("Asset ID is required");
}

final PermissionableObjectView view = assetPermissionHelper.getPermissionableObjectView(
assetId, user);

return new ResponseEntityPermissionableObjectView(view);
}
}
Loading
Loading