From 9bc9bf5331a306f93c17269b72dfb3a6633ef8c1 Mon Sep 17 00:00:00 2001 From: kwahlin Date: Wed, 8 Nov 2023 15:26:33 +0100 Subject: [PATCH 1/2] Allow some outgoing relations to link to deleted --- .../src/main/groovy/whelk/Document.groovy | 1 + .../component/PostgreSQLComponent.groovy | 5 +++- .../groovy/whelk/filter/LinkFinder.groovy | 28 +++++++++++++------ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/whelk-core/src/main/groovy/whelk/Document.groovy b/whelk-core/src/main/groovy/whelk/Document.groovy index 0580d6de04..fa398f3f1f 100644 --- a/whelk-core/src/main/groovy/whelk/Document.groovy +++ b/whelk-core/src/main/groovy/whelk/Document.groovy @@ -49,6 +49,7 @@ class Document { static final List thingIndirectTypedIDsPath = ["@graph", 1, "indirectlyIdentifiedBy"] static final List thingCarrierTypesPath = ["@graph", 1, "carrierType"] static final List thingInSchemePath = ["@graph",1,"inScheme","@id"] + static final List recordPath = ["@graph", 0] static final List recordIdPath = ["@graph", 0, "@id"] static final List workIdPath = ["@graph", 1, "instanceOf", "@id"] static final List thingMetaPath = ["@graph", 1, "meta", "@id"] diff --git a/whelk-core/src/main/groovy/whelk/component/PostgreSQLComponent.groovy b/whelk-core/src/main/groovy/whelk/component/PostgreSQLComponent.groovy index f005b24a42..11c762ea68 100644 --- a/whelk-core/src/main/groovy/whelk/component/PostgreSQLComponent.groovy +++ b/whelk-core/src/main/groovy/whelk/component/PostgreSQLComponent.groovy @@ -1955,7 +1955,10 @@ class PostgreSQLComponent { } } - boolean iriIsLinkable(String iri) { + boolean iriIsLinkable(String iri, String path) { + if (path in JsonLd.ALLOW_LINK_TO_DELETED) { + return true + } withDbConnection { PreparedStatement preparedStatement = null ResultSet rs = null diff --git a/whelk-core/src/main/groovy/whelk/filter/LinkFinder.groovy b/whelk-core/src/main/groovy/whelk/filter/LinkFinder.groovy index 21eb1099d2..67ac57e512 100644 --- a/whelk-core/src/main/groovy/whelk/filter/LinkFinder.groovy +++ b/whelk-core/src/main/groovy/whelk/filter/LinkFinder.groovy @@ -114,11 +114,18 @@ class LinkFinder { items[0][JsonLd.THING_KEY]['@id'] = items[1]['@id'] } - private void replaceSameAsLinksWithPrimaries(Map data) { + private void replaceSameAsLinksWithPrimaries(Map data, List path = []) { // If this is a link (an object containing _only_ an id) + String id = data.get("@id") if (id != null && data.keySet().size() == 1) { - String primaryId = lookupPrimaryId(id) + String normalizedPath = (path.take(2) == Document.recordPath + ? [JsonLd.RECORD_KEY] + path.drop(2) + : (path.take(2) == Document.thingPath ? path.drop(2) : path) + ) + .findAll { it instanceof String } + .join('.') + String primaryId = lookupPrimaryId(id, normalizedPath) if (primaryId != null) data.put("@id", primaryId) } @@ -134,29 +141,32 @@ class LinkFinder { Object value = data.get(key) if (value instanceof List) - replaceSameAsLinksWithPrimaries( (List) value ) + replaceSameAsLinksWithPrimaries( (List) value, path + keyString ) if (value instanceof Map) - replaceSameAsLinksWithPrimaries( (Map) value ) + replaceSameAsLinksWithPrimaries( (Map) value, path + keyString ) } } - private void replaceSameAsLinksWithPrimaries(List data) { + private void replaceSameAsLinksWithPrimaries(List data, List path) { + int idx = 0 for (Object element : data){ if (element instanceof List) - replaceSameAsLinksWithPrimaries( (List) element ) + replaceSameAsLinksWithPrimaries( (List) element, path + idx ) else if (element instanceof Map) - replaceSameAsLinksWithPrimaries( (Map) element ) + replaceSameAsLinksWithPrimaries( (Map) element, path + idx ) + idx += 1 } } - private String lookupPrimaryId(String id) { + private String lookupPrimaryId(String id, String path) { String mainIri = postgres.getMainId(id) if (mainIri == null) return null - if (postgres.iriIsLinkable(mainIri)) + if (postgres.iriIsLinkable(mainIri, path)) return mainIri + throw new LinkValidationException("Not allowed to link to the deleted resource: " + mainIri) } From 3da67ed11503a2226d337ae225be628d3fa522b4 Mon Sep 17 00:00:00 2001 From: kwahlin Date: Thu, 16 Nov 2023 09:44:03 +0100 Subject: [PATCH 2/2] Be explicit about which links are forbidden in error message --- rest/src/main/groovy/whelk/rest/api/Crud.groovy | 16 ++++++++++------ .../whelk/component/PostgreSQLComponent.groovy | 9 +++++++-- .../main/groovy/whelk/filter/LinkFinder.groovy | 3 ++- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/rest/src/main/groovy/whelk/rest/api/Crud.groovy b/rest/src/main/groovy/whelk/rest/api/Crud.groovy index 6bb99fa477..41587f28b2 100644 --- a/rest/src/main/groovy/whelk/rest/api/Crud.groovy +++ b/rest/src/main/groovy/whelk/rest/api/Crud.groovy @@ -823,13 +823,17 @@ class Crud extends HttpServlet { throw new OtherStatusException("You do not have sufficient privileges to perform this operation.", HttpServletResponse.SC_FORBIDDEN) } else if (doc && doc.deleted) { throw new OtherStatusException("Document has been deleted.", HttpServletResponse.SC_GONE) - } else if(!whelk.storage.followDependers(doc.getShortId(), JsonLd.ALLOW_LINK_TO_DELETED).isEmpty()) { - throw new OtherStatusException("This record may not be deleted, because it is referenced by other records.", HttpServletResponse.SC_FORBIDDEN) } else { - log.debug("Removing resource at ${doc.getShortId()}") - String activeSigel = request.getHeader(XL_ACTIVE_SIGEL_HEADER) - whelk.remove(doc.getShortId(), "xl", activeSigel) - response.setStatus(HttpServletResponse.SC_NO_CONTENT) + def referencedBy = whelk.storage.followDependers(doc.getShortId(), JsonLd.ALLOW_LINK_TO_DELETED) + if (!referencedBy.isEmpty()) { + def referencedByStr = referencedBy.collect { shortId, path -> "$shortId at $path" }.join(', ') + throw new OtherStatusException("This record may not be deleted, because it is referenced by other records: " + referencedByStr, HttpServletResponse.SC_FORBIDDEN) + } else { + log.debug("Removing resource at ${doc.getShortId()}") + String activeSigel = request.getHeader(XL_ACTIVE_SIGEL_HEADER) + whelk.remove(doc.getShortId(), "xl", activeSigel) + response.setStatus(HttpServletResponse.SC_NO_CONTENT) + } } } diff --git a/whelk-core/src/main/groovy/whelk/component/PostgreSQLComponent.groovy b/whelk-core/src/main/groovy/whelk/component/PostgreSQLComponent.groovy index 11c762ea68..6a8e9dcc63 100644 --- a/whelk-core/src/main/groovy/whelk/component/PostgreSQLComponent.groovy +++ b/whelk-core/src/main/groovy/whelk/component/PostgreSQLComponent.groovy @@ -2588,8 +2588,13 @@ class PostgreSQLComponent { void remove(String identifier, String changedIn, String changedBy, boolean force=false) { if (versioning) { - if(!force && !followDependers(identifier, JsonLd.ALLOW_LINK_TO_DELETED).isEmpty()) - throw new RuntimeException("Deleting depended upon records is not allowed.") + if (!force) { + def referencedBy = followDependers(identifier, JsonLd.ALLOW_LINK_TO_DELETED) + if (!referencedBy.isEmpty()) { + def referencedByStr = referencedBy.collect { shortId, path -> "$shortId at $path" }.join(', ') + throw new RuntimeException("Deleting depended upon records is not allowed. Referenced by: $referencedByStr") + } + } log.debug("Marking document with ID ${identifier} as deleted.") try { diff --git a/whelk-core/src/main/groovy/whelk/filter/LinkFinder.groovy b/whelk-core/src/main/groovy/whelk/filter/LinkFinder.groovy index 67ac57e512..d5e6f92806 100644 --- a/whelk-core/src/main/groovy/whelk/filter/LinkFinder.groovy +++ b/whelk-core/src/main/groovy/whelk/filter/LinkFinder.groovy @@ -119,6 +119,7 @@ class LinkFinder { String id = data.get("@id") if (id != null && data.keySet().size() == 1) { + // Path to same form as in lddb__dependencies.relation String normalizedPath = (path.take(2) == Document.recordPath ? [JsonLd.RECORD_KEY] + path.drop(2) : (path.take(2) == Document.thingPath ? path.drop(2) : path) @@ -167,7 +168,7 @@ class LinkFinder { if (postgres.iriIsLinkable(mainIri, path)) return mainIri - throw new LinkValidationException("Not allowed to link to the deleted resource: " + mainIri) + throw new LinkValidationException("Forbidden link to deleted resource $mainIri found at $path") } /**