Skip to content
Permalink
Browse files
feat(customIRIs): custom IRIs must contain a UUID (DSP-1763) (#1884)
* feat (customIRIs): custom IRIs must contain a UUID

* fix(customIRIs): fix the custom IRIs given to new permissions of a new project

* fix (customIRIs): fix failing tests

* fix (customIRI): custom project IRI can contain UUID instead of project shortcode,

* docs (custom IRIs): update documentation

* fix (customIRI): custom value UUID should be part of custom value IRI

* fix (customIri): fix the failing tests

* docs (customIRI): add documentation about breaking changes of V14

* refactor(customIRI): clean up

Co-authored-by: Ivan Subotic <400790+subotic@users.noreply.github.com>
  • Loading branch information
SepidehAlassi and subotic committed Jun 29, 2021
1 parent 2424047 commit 593d9cb30a7fb332f8062898bcfa07abf1e7951d
Showing with 380 additions and 194 deletions.
  1. +1 −1 docs/03-apis/api-admin/groups.md
  2. +13 −13 docs/03-apis/api-admin/lists.md
  3. +8 −8 docs/03-apis/api-admin/lists_new-list-admin-routes_v1.md
  4. +3 −3 docs/03-apis/api-admin/permissions.md
  5. +1 −1 docs/03-apis/api-admin/projects.md
  6. +1 −1 docs/03-apis/api-admin/users.md
  7. +2 −2 docs/03-apis/api-v2/editing-resources.md
  8. +7 −4 docs/03-apis/api-v2/editing-values.md
  9. +1 −1 docs/03-apis/api-v2/knora-iris.md
  10. +8 −0 docs/05-internals/development/migrationNotes.md
  11. +7 −3 webapi/src/main/scala/org/knora/webapi/messages/StringFormatter.scala
  12. +14 −6 webapi/src/main/scala/org/knora/webapi/responders/Responder.scala
  13. +5 −11 webapi/src/main/scala/org/knora/webapi/responders/admin/ProjectsResponderADM.scala
  14. +46 −20 webapi/src/main/scala/org/knora/webapi/responders/v2/ValuesResponderV2.scala
  15. +5 −6 webapi/src/test/scala/org/knora/webapi/e2e/admin/GroupsADME2ESpec.scala
  16. +7 −7 webapi/src/test/scala/org/knora/webapi/e2e/admin/PermissionsADME2ESpec.scala
  17. +3 −5 webapi/src/test/scala/org/knora/webapi/e2e/admin/ProjectsADME2ESpec.scala
  18. +7 −5 webapi/src/test/scala/org/knora/webapi/e2e/admin/UsersADME2ESpec.scala
  19. +2 −5 webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/NewListsRoutesADMFeatureE2ESpec.scala
  20. +2 −5 webapi/src/test/scala/org/knora/webapi/e2e/admin/lists/OldListsRouteADMFeatureE2ESpec.scala
  21. +62 −16 webapi/src/test/scala/org/knora/webapi/e2e/v2/ValuesRouteV2E2ESpec.scala
  22. +6 −4 webapi/src/test/scala/org/knora/webapi/responders/admin/PermissionsResponderADMSpec.scala
  23. +58 −59 webapi/src/test/scala/org/knora/webapi/responders/admin/ProjectsResponderADMSpec.scala
  24. +105 −2 webapi/src/test/scala/org/knora/webapi/responders/v2/ResourcesResponderV2Spec.scala
  25. +6 −6 webapi/src/test/scala/org/knora/webapi/sharedtestdata/SharedTestDataADM.scala
@@ -63,7 +63,7 @@ specified by the `id` in the request body as below:

```json
{
"id": "http://rdfh.ch/groups/00FF/group-with-custom-Iri",
"id": "http://rdfh.ch/groups/00FF/a95UWs71KUklnFOe1rcw1w",
"name": "GroupWithCustomIRI",
"description": "A new group with a custom IRI",
"project": "http://rdfh.ch/projects/00FF",
@@ -75,7 +75,7 @@ Additionally, each list can have an optional custom IRI (of [Knora IRI](../api-v

```json
{
"id": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "a new list",
"labels": [{ "value": "Neue Liste mit IRI", "language": "de"}],
@@ -90,7 +90,7 @@ The response will contain the basic information of the list, `listinfo` and an e
"children": [],
"listinfo": {
"comments": [],
"id": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"isRootNode": true,
"labels": [
{
@@ -121,7 +121,7 @@ list and the IRI of the project it belongs to.
- BODY:

```json
{ "listIri": "http://rdfh.ch/lists/0001/a-list",
{ "listIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "new name for the list",
"labels": [{ "value": "a new label for the list", "language": "en"}],
@@ -139,7 +139,7 @@ The response will contain the basic information of the list, `listinfo`, without
"language": "en"
}
],
"id": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"isRootNode": true,
"labels": [
{
@@ -232,7 +232,7 @@ There is no need to specify the project IRI because it is automatically extracte

```json
{
"parentNodeIri": "http://rdfh.ch/lists/0001/a-list",
"parentNodeIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "a child",
"labels": [{ "value": "New List Node", "language": "en"}],
@@ -243,8 +243,8 @@ There is no need to specify the project IRI because it is automatically extracte
Additionally, each child node can have an optional custom IRI (of [Knora IRI](../api-v2/knora-iris.md#iris-for-data) form) specified by the `id` in the request body as below:

```json
{ "id": "http://rdfh.ch/lists/0001/a-childNode",
"parentNodeIri": "http://rdfh.ch/lists/0001/a-list",
{ "id": "http://rdfh.ch/lists/0001/8u37MxBVMbX3XQ8-d31x6w",
"parentNodeIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "a child",
"labels": [{ "value": "New List Node", "language": "en"}],
@@ -257,8 +257,8 @@ The response will contain the basic information of the node, `nodeinfo`, as belo
{
"nodeinfo": {
"comments": [],
"hasRootNode": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/a-childNode",
"hasRootNode": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"id": "http://rdfh.ch/lists/0001/8u37MxBVMbX3XQ8-d31x6w",
"labels": [
{
"value": "New List Node",
@@ -275,7 +275,7 @@ according to the given position, the sibling nodes will be shifted. Note that `p
number of existing children.

```json
{ "parentNodeIri": "http://rdfh.ch/lists/0001/a-list",
{ "parentNodeIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "Inserted new child",
"position": 0,
@@ -298,7 +298,7 @@ node and the IRI of the project it belongs to.
- BODY:

```json
{ "listIri": "http://rdfh.ch/lists/0001/a-childNode",
{ "listIri": "http://rdfh.ch/lists/0001/8u37MxBVMbX3XQ8-d31x6w",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "new node name",
"labels": [{ "value": "new node label", "language": "en"}],
@@ -317,8 +317,8 @@ The response will contain the basic information of the node as `nodeInfo` withou
"language": "en"
}
],
"hasRootNode": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/a-childNode",
"hasRootNode": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"id": "http://rdfh.ch/lists/0001/8u37MxBVMbX3XQ8-d31x6w",
"labels": [
{
"value": "new node label",
@@ -94,7 +94,7 @@ Additionally, each list can have an optional custom IRI (of [Knora IRI](../api-v

```json
{
"id": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "a new list",
"labels": [{ "value": "Neue Liste mit IRI", "language": "de"}],
@@ -110,7 +110,7 @@ The response will contain the basic information of the list, `listinfo` and an e
"children": [],
"listinfo": {
"comments": [],
"id": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"isRootNode": true,
"labels": [
{
@@ -131,7 +131,7 @@ Furthermore, the request body should also contain the project IRI of the list an

```json
{
"parentNodeIri": "http://rdfh.ch/lists/0001/a-list",
"parentNodeIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "a child",
"labels": [{ "value": "New List Node", "language": "en"}],
@@ -142,8 +142,8 @@ Furthermore, the request body should also contain the project IRI of the list an
Additionally, each child node can have an optional custom IRI (of [Knora IRI](../api-v2/knora-iris.md#iris-for-data) form) specified by the `id` in the request body as below:

```json
{ "id": "http://rdfh.ch/lists/0001/a-childNode",
"parentNodeIri": "http://rdfh.ch/lists/0001/a-list",
{ "id": "http://rdfh.ch/lists/0001/8u37MxBVMbX3XQ8-d31x6w",
"parentNodeIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "a child",
"labels": [{ "value": "New List Node", "language": "en"}],
@@ -157,8 +157,8 @@ The response will contain the basic information of the node, `nodeinfo`, as belo
{
"nodeinfo": {
"comments": [],
"hasRootNode": "http://rdfh.ch/lists/0001/a-list",
"id": "http://rdfh.ch/lists/0001/a-childNode",
"hasRootNode": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"id": "http://rdfh.ch/lists/0001/8u37MxBVMbX3XQ8-d31x6w",
"labels": [
{
"value": "New List Node",
@@ -176,7 +176,7 @@ according to the given position, the sibling nodes will be shifted. Note that `p
number of existing children.

```json
{ "parentNodeIri": "http://rdfh.ch/lists/0001/a-list",
{ "parentNodeIri": "http://rdfh.ch/lists/0001/yWQEGXl53Z4C4DYJ-S2c5A",
"projectIri": "http://rdfh.ch/projects/0001",
"name": "Inserted new child",
"position": 0,
@@ -58,7 +58,7 @@ the `@id` attribute which will then be assigned to the permission; otherwise the
A custom permission IRI must be `http://rdfh.ch/permissions/PROJECT_SHORTCODE/` (where `PROJECT_SHORTCODE`
is the shortcode of the project that the permission belongs to), plus a custom ID string. For example:
```
"id": "http://rdfh.ch/permissions/0001/AP-with-customIri",
"id": "http://rdfh.ch/permissions/0001/jKIYuaEUETBcyxpenUwRzQ",
```

As a response, the created administrative permission and its IRI are returned as below:
@@ -108,7 +108,7 @@ a resource class of a specific project:

```json
{
"id": "http://rdfh.ch/permissions/00FF/DOAP-with-customIri",
"id": "http://rdfh.ch/permissions/00FF/fSw7w1sI5IwDjEfFi1jOeQ",
"forGroup":null,
"forProject":"http://rdfh.ch/projects/00FF",
"forProperty":null,
@@ -133,7 +133,7 @@ The response contains the newly created permission and its IRI, as:
"permissionCode": 7
}
],
"iri": "http://rdfh.ch/permissions/00FF/DOAP-with-customIri"
"iri": "http://rdfh.ch/permissions/00FF/fSw7w1sI5IwDjEfFi1jOeQ"
}
}
```
@@ -89,7 +89,7 @@ Additionally, each project can have an optional custom IRI (of [Knora IRI](../ap

```json
{
"id": "http://rdfh.ch/projects/3333",
"id": "http://rdfh.ch/projects/9TaSVMUuiRhQsuWHDPr8rw",
"shortname": "newprojectWithIri",
"shortcode": "3333",
"longname": "new project with a custom IRI",
@@ -96,7 +96,7 @@ specified by the `id` in the request body as below:

```json
{
"id" : "http://rdfh.ch/users/donaldDuck",
"id" : "http://rdfh.ch/users/FnjFfIQFVDvI7ex8zSyUyw",
"email": "donald.duck@example.org",
"givenName": "Donald",
"familyName": "Duck",
@@ -202,13 +202,13 @@ For example:

```jsonld
{
"@id" : "http://rdfh.ch/0001/a-custom-thing",
"@id" : "http://rdfh.ch/0001/oveR1dQltEUwNrls9Lu5Rw",
"@type" : "anything:Thing",
"knora-api:attachedToProject" : {
"@id" : "http://rdfh.ch/projects/0001"
},
"anything:hasInteger" : {
"@id" : "http://rdfh.ch/0001/a-custom-thing/values/int-value-IRI",
"@id" : "http://rdfh.ch/0001/oveR1dQltEUwNrls9Lu5Rw/values/IN4R19yYR0ygi3K2VEHpUQ",
"@type" : "knora-api:IntValue",
"knora-api:intValueAsInt" : 10,
"knora-api:valueHasUUID" : "IN4R19yYR0ygi3K2VEHpUQ",
@@ -84,9 +84,12 @@ Permissions for the new value can be given by adding `knora-api:hasPermissions`.
}
```

Each value can have an optional custom IRI (of [Knora IRI](knora-iris.md#iris-for-data) form) specified by the `@id` attribute, a custom creation date specified by adding
`knora-api:valueCreationDate` (an [xsd:dateTimeStamp](https://www.w3.org/TR/xmlschema11-2/#dateTimeStamp)), or a custom UUID
given by `knora-api:valueHasUUID`. Each custom UUID must be [base64url-encoded](rfc:4648#section-5), without padding.
Each value can have an optional custom IRI (of [Knora IRI](knora-iris.md#iris-for-data) form) specified by the `@id` attribute,
a custom creation date specified by adding `knora-api:valueCreationDate` (an [xsd:dateTimeStamp](https://www.w3.org/TR/xmlschema11-2/#dateTimeStamp)),
or a custom UUID given by `knora-api:valueHasUUID`. Each custom UUID must be [base64url-encoded](rfc:4648#section-5), without padding.
If a custom UUID is provided, it will be used in value IRI. If a custom IRI is given for the value, its UUID should match
the given custom UUID. If a custom IRI is provided, but there
is no custom UUID provided, then the UUID given in the IRI will be assigned to the `knora-api:valueHasUUID`.
A custom value IRI must be the IRI of the containing resource, followed
by a `/values/` and a custom ID string. For example:

@@ -95,7 +98,7 @@ by a `/values/` and a custom ID string. For example:
"@id" : "http://rdfh.ch/0001/a-thing",
"@type" : "anything:Thing",
"anything:hasInteger" : {
"@id" : "http://rdfh.ch/0001/a-thing/values/int-value-IRI",
"@id" : "http://rdfh.ch/0001/a-thing/values/IN4R19yYR0ygi3K2VEHpUQ",
"@type" : "knora-api:IntValue",
"knora-api:intValueAsInt" : 21,
"knora-api:valueHasUUID" : "IN4R19yYR0ygi3K2VEHpUQ",
@@ -225,7 +225,7 @@ follows:
`http://rdfh.ch/PROJECT_SHORTCODE/mappings/MAPPING_NAME`
- XML-to-standoff mapping element:
`http://rdfh.ch/PROJECT_SHORTCODE/mappings/MAPPING_NAME/elements/MAPPING_ELEMENT_UUID`
- Project: `http://rdfh.ch/projects/PROJECT_SHORTCODE`
- Project: `http://rdfh.ch/projects/PROJECT_SHORTCODE` (or `http://rdfh.ch/projects/PROJECT_UUID`)
- Group: `http://rdfh.ch/groups/PROJECT_SHORTCODE/GROUP_UUID`
- Permission:
`http://rdfh.ch/permissions/PROJECT_SHORTCODE/PERMISSION_UUID`
@@ -0,0 +1,8 @@
#Breaking Changes and Migration Notes

##dsp-api V14
We are slowly moving towards unifying the form of all entity IRIs (project, user, resource, value, etc.). All these
entities should end with a unique base64Encoded-UUID without padding as 22-characters string. Following breaking changes
are implemented:
- Enforce all custom IRIs given to entities during creation to end with a valid base64Encoded UUID
([PR #1884](https://github.com/dasch-swiss/dsp-api/pull/1884)).
@@ -3068,11 +3068,15 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No
* Creates a new value IRI based on a UUID.
*
* @param resourceIri the IRI of the resource that will contain the value.
* @param givenUUID the optional given UUID of the value. If not provided, create a random one.
* @return a new value IRI.
*/
def makeRandomValueIri(resourceIri: IRI): IRI = {
val knoraValueUuid = makeRandomBase64EncodedUuid
s"$resourceIri/values/$knoraValueUuid"
def makeRandomValueIri(resourceIri: IRI, givenUUID: Option[UUID] = None): IRI = {
val valueUUID = givenUUID match {
case Some(uuid: UUID) => base64EncodeUuid(uuid)
case _ => makeRandomBase64EncodedUuid
}
s"$resourceIri/values/$valueUUID"
}

/**
@@ -20,13 +20,12 @@
package org.knora.webapi
package responders

import exceptions.{DuplicateValueException, UnexpectedMessageException}
import exceptions.{BadRequestException, DuplicateValueException, UnexpectedMessageException}
import messages.store.triplestoremessages.SparqlSelectRequest
import messages.util.ResponderData
import messages.util.rdf.SparqlSelectResult
import messages.{SmartIri, StringFormatter}
import settings.{KnoraDispatchers, KnoraSettings, KnoraSettingsImpl}

import akka.actor.{ActorRef, ActorSystem}
import akka.event.LoggingAdapter
import akka.http.scaladsl.util.FastFuture
@@ -174,14 +173,23 @@ abstract class Responder(responderData: ResponderData) extends LazyLogging {
* @return IRI of the entity.
*/
protected def checkOrCreateEntityIri(entityIri: Option[SmartIri], iriFormatter: => IRI): Future[IRI] = {

entityIri match {
case Some(customResourceIri) =>
case Some(customEntityIri: SmartIri) =>
val entityIriAsString = customEntityIri.toString
for {
result <- stringFormatter.checkIriExists(customResourceIri.toString, storeManager)

result <- stringFormatter.checkIriExists(entityIriAsString, storeManager)
_ = if (result) {
throw DuplicateValueException(s"IRI: '${customResourceIri.toString}' already exists, try another one.")
throw DuplicateValueException(s"IRI: '$entityIriAsString' already exists, try another one.")
}
} yield customResourceIri.toString
// Check that given entityIRI ends with a UUID
ending: String = entityIriAsString.split('/').last
_ = stringFormatter.validateBase64EncodedUuid(
ending,
throw BadRequestException(s"IRI: '$entityIriAsString' must end with a valid base 64 UUID."))

} yield entityIriAsString

case None => stringFormatter.makeUnusedIri(iriFormatter, storeManager, loggingAdapter)
}
@@ -31,7 +31,6 @@ import org.knora.webapi.exceptions._
import org.knora.webapi.feature.FeatureFactoryConfig
import org.knora.webapi.instrumentation.InstrumentationSupport
import org.knora.webapi.messages.IriConversions._
import org.knora.webapi.messages.StringFormatter.IriDomain
import org.knora.webapi.messages.admin.responder.projectsmessages._
import org.knora.webapi.messages.admin.responder.usersmessages.{
UserADM,
@@ -982,11 +981,9 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo
*/
def createPermissionsForAdminsAndMembersOfNewProject(projectIri: IRI, projectShortCode: String): Future[Unit] =
for {
baseIri: String <- Future.successful(s"http://$IriDomain/permissions/$projectShortCode/")
// Give the admins of the new project rights for any operation in project level, and rights to create resources.
apPermissionForProjectAdmin: AdministrativePermissionCreateResponseADM <- (responderManager ? AdministrativePermissionCreateRequestADM(
_ <- (responderManager ? AdministrativePermissionCreateRequestADM(
createRequest = CreateAdministrativePermissionAPIRequestADM(
id = Some(baseIri + "defaultApForAdmin"),
forProject = projectIri,
forGroup = OntologyConstants.KnoraAdmin.ProjectAdmin,
hasPermissions =
@@ -998,9 +995,8 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo
)).mapTo[AdministrativePermissionCreateResponseADM]

// Give the members of the new project rights to create resources.
apPermissionForProjectMember: AdministrativePermissionCreateResponseADM <- (responderManager ? AdministrativePermissionCreateRequestADM(
_ <- (responderManager ? AdministrativePermissionCreateRequestADM(
createRequest = CreateAdministrativePermissionAPIRequestADM(
id = Some(baseIri + "defaultApForMember"),
forProject = projectIri,
forGroup = OntologyConstants.KnoraAdmin.ProjectMember,
hasPermissions = Set(PermissionADM.ProjectResourceCreateAllPermission)
@@ -1012,9 +1008,8 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo

// Give the admins of the new project rights to change rights, modify, delete, view,
// and restricted view of all resources and values that belong to the project.
doapForProjectAdmin <- (responderManager ? DefaultObjectAccessPermissionCreateRequestADM(
_ <- (responderManager ? DefaultObjectAccessPermissionCreateRequestADM(
createRequest = CreateDefaultObjectAccessPermissionAPIRequestADM(
id = Some(baseIri + "defaultDoapForAdmin"),
forProject = projectIri,
forGroup = Some(OntologyConstants.KnoraAdmin.ProjectAdmin),
hasPermissions = Set(
@@ -1032,9 +1027,8 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo

// Give the members of the new project rights to modify, view, and restricted view of all resources and values
// that belong to the project.
doapForProjectMember <- (responderManager ? DefaultObjectAccessPermissionCreateRequestADM(
_ <- (responderManager ? DefaultObjectAccessPermissionCreateRequestADM(
createRequest = CreateDefaultObjectAccessPermissionAPIRequestADM(
id = Some(baseIri + "defaultDoapForMember"),
forProject = projectIri,
forGroup = Some(OntologyConstants.KnoraAdmin.ProjectMember),
hasPermissions = Set(
@@ -1109,7 +1103,7 @@ class ProjectsResponderADM(responderData: ResponderData) extends Responder(respo
)
.toString

createProjectResponse <- (storeManager ? SparqlUpdateRequest(createNewProjectSparqlString))
_ <- (storeManager ? SparqlUpdateRequest(createNewProjectSparqlString))
.mapTo[SparqlUpdateResponse]

// try to retrieve newly created project (will also add to cache)

0 comments on commit 593d9cb

Please sign in to comment.