Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(projects)!: Change shortname to xsd:NCName forma, Escape special…
… character in payloads of projects endpoints (DSP-1555 ) (#1886)

* fix (projects): validate and escape project parameters

* fix (projects): escape special characters before putting in triplestore, and unescape again before returning the response

* docs (projects): update docs

* fix (projects): fix the failing tests

* fix (projects): fix the failing test in v1
  • Loading branch information
SepidehAlassi committed Jun 30, 2021
1 parent 593d9cb commit b3c2d5f
Show file tree
Hide file tree
Showing 10 changed files with 319 additions and 164 deletions.
29 changes: 18 additions & 11 deletions docs/03-apis/api-admin/projects.md
Expand Up @@ -58,34 +58,40 @@ License along with DSP. If not, see <http://www.gnu.org/licenses/>.
### Create a new project:

- Required permission: SystemAdmin
- Required information: shortname (unique; used for named graphs),
status, selfjoin
- Optional information: longname, description, keywords, logo
- Required information:
- shortcode (unique, 4-digits)
- shortname (unique; it should be in the form of a
[xsd:NCNAME](https://www.w3.org/TR/xmlschema11-2/#NCName))
- description (collection of descriptions as strings with language tag.)
- keywords (collection of keywords)
- status (true, if project is active. false, if project is inactive)
- selfjoin
- Optional information: longname, logo
- Returns information about the newly created project
- Remark: There are two distinct use cases / payload combination:

(1) change ontology and data graph: ontologygraph, datagraph,

(2) basic project information: shortname, longname, description,
(2) basic project information: shortcode, shortname, longname, description,
keywords, logo, institution, status, selfjoin

- TypeScript Docs: projectFormats - CreateProjectApiRequestV1
- POST: `/admin/projects/`
- BODY:

```json
{
"shortname": "newproject",
"longname": "project longname",
"description": "project description",
"keywords": "keywords",
"description": [{"value": "project description", "language": "en"}],
"keywords": ["test project"],
"logo": "/fu/bar/baz.jpg",
"status": true,
"selfjoin": false
}
```

Additionally, each project 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:
Additionally, each project 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
{
Expand All @@ -100,6 +106,7 @@ Additionally, each project can have an optional custom IRI (of [Knora IRI](../ap
"selfjoin": false
}
```

#### Default set of permissions for a new project:
When a new project is created, following default permissions are added to its admins and members:
- ProjectAdmin group receives an administrative permission to do all project level operations and to create resources
Expand All @@ -123,7 +130,7 @@ belongs to the project. This default object access permission is retrievable thr

- Required permission: SystemAdmin / ProjectAdmin
- Changeable information: shortname, longname, description,
keywords, logo, status, selfjoin
keywords, logo, status, selfjoin. The payload must at least contain a new value for one of these properties.
- TypeScript Docs: projectFormats - ChangeProjectApiRequestV1
- PUT: `/admin/projects/iri/<projectIri>`
- BODY:
Expand All @@ -132,8 +139,8 @@ belongs to the project. This default object access permission is retrievable thr
{
"shortname": "newproject",
"longname": "project longname",
"description": "project description",
"keywords": "keywords",
"description": [{"value": "a new description", "language": "en"}],
"keywords": ["a new key"],
"logo": "/fu/bar/baz.jpg",
"status": true,
"selfjoin": false
Expand Down
Expand Up @@ -885,14 +885,6 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No
private val UsernameRegex: Regex =
"""^(?=.{4,50}$)(?![_.])(?!.*[_.]{2})[a-zA-Z0-9._]+(?<![_.])$""".r

/**
* A regex that matches a valid project shortname
* - 4 - 50 characters long
* - Only contains alphanumeric characters.
*/
private val ProjectShortnameRegex: Regex =
"""^(?=.{4,50}$)[a-zA-Z0-9]+$""".r

/**
* The information that is stored about non-Knora IRIs.
*/
Expand Down Expand Up @@ -2450,33 +2442,6 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No
OntologyConstants.NamedGraphs.DataNamedGraphStart + "/" + project.shortcode + "/" + project.shortname + "/metadata"
}

/**
* Given the project IRI, checks if it is in a valid format.
*
* @param iri the project's IRI.
* @return the IRI of the project.
*/
def validateProjectIri(iri: IRI, errorFun: => Nothing): IRI = {
if (isKnoraProjectIriStr(iri)) {
iri
} else {
errorFun
}
}

/**
* Given the optional project IRI, checks if it is in a valid format.
*
* @param maybeIri the optional project's IRI to be checked.
* @return the same optional IRI.
*/
def validateOptionalProjectIri(maybeIri: Option[IRI], errorFun: => Nothing): Option[IRI] = {
maybeIri match {
case Some(iri) => Some(validateProjectIri(iri, errorFun))
case None => None
}
}

/**
* Check that the supplied IRI represents a valid project IRI.
*
Expand All @@ -2501,7 +2466,7 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No
* project IRI.
* @return the same optional string but escaped.
*/
def validateAndEscapeOptionalProjectIri(maybeString: Option[String], errorFun: => Nothing): Option[String] = {
def validateAndEscapeOptionalProjectIri(maybeString: Option[String], errorFun: => Nothing): Option[IRI] = {
maybeString match {
case Some(s) => Some(validateAndEscapeProjectIri(s, errorFun))
case None => None
Expand All @@ -2517,7 +2482,7 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No
* @return the same string.
*/
def validateAndEscapeProjectShortname(value: String, errorFun: => Nothing): String = {
ProjectShortnameRegex.findFirstIn(value) match {
NCNameRegex.findFirstIn(value) match {
case Some(shortname) => toSparqlEncodedString(shortname, errorFun)
case None => errorFun
}
Expand Down Expand Up @@ -2594,6 +2559,14 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No
}
}

def escapeOptionalString(maybeString: Option[String], errorFun: => Nothing): Option[String] = {
maybeString match {
case Some(s) =>
Some(toSparqlEncodedString(s, errorFun))
case None => None
}
}

/**
* Given the list IRI, checks if it is in a valid format.
*
Expand Down Expand Up @@ -3263,4 +3236,10 @@ class StringFormatter private (val maybeSettings: Option[KnoraSettingsImpl] = No
StringLiteralV2(value = fromSparqlEncodedString(stringLiteral.value), language = stringLiteral.language))
)
}
def unescapeOptionalString(optionalString: Option[String]): Option[String] = {
optionalString match {
case Some(s: String) => Some(fromSparqlEncodedString(s))
case None => None
}
}
}
Expand Up @@ -54,13 +54,13 @@ case class CreateAdministrativePermissionAPIRequestADM(id: Option[IRI] = None,
def toJsValue: JsValue = createAdministrativePermissionAPIRequestADMFormat.write(this)

implicit protected val stringFormatter: StringFormatter = StringFormatter.getInstanceForConstantOntologies
stringFormatter.validateProjectIri(forProject, throw BadRequestException(s"Invalid project IRI"))
stringFormatter.validateAndEscapeProjectIri(forProject, throw BadRequestException(s"Invalid project IRI $forProject"))
stringFormatter.validateOptionalPermissionIri(
id,
throw BadRequestException(s"Invalid permission IRI ${id.get} is given."))
if (hasPermissions.isEmpty) throw BadRequestException("Permissions needs to be supplied.")
if (!OntologyConstants.KnoraAdmin.BuiltInGroups.contains(forGroup)) {
stringFormatter.validateGroupIri(forGroup, throw BadRequestException(s"Invalid group IRI ${forGroup}"))
stringFormatter.validateGroupIri(forGroup, throw BadRequestException(s"Invalid group IRI $forGroup"))
}
}

Expand All @@ -84,7 +84,7 @@ case class CreateDefaultObjectAccessPermissionAPIRequestADM(id: Option[IRI] = No
def toJsValue: JsValue = createDefaultObjectAccessPermissionAPIRequestADMFormat.write(this)

implicit protected val stringFormatter: StringFormatter = StringFormatter.getInstanceForConstantOntologies
stringFormatter.validateProjectIri(forProject, throw BadRequestException(s"Invalid project IRI"))
stringFormatter.validateAndEscapeProjectIri(forProject, throw BadRequestException(s"Invalid project IRI $forProject"))
stringFormatter.validateOptionalPermissionIri(
id,
throw BadRequestException(s"Invalid permission IRI ${id.get} is given."))
Expand Down Expand Up @@ -231,7 +231,7 @@ case class PermissionsForProjectGetRequestADM(projectIri: IRI,
extends PermissionsResponderRequestADM {

implicit protected val stringFormatter: StringFormatter = StringFormatter.getInstanceForConstantOntologies
stringFormatter.validateProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI"))
stringFormatter.validateAndEscapeProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI $projectIri"))

// Check user's permission for the operation
if (!requestingUser.isSystemAdmin
Expand Down Expand Up @@ -345,7 +345,7 @@ case class AdministrativePermissionsForProjectGetRequestADM(projectIri: IRI,
extends PermissionsResponderRequestADM {

implicit protected val stringFormatter: StringFormatter = StringFormatter.getInstanceForConstantOntologies
stringFormatter.validateProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI"))
stringFormatter.validateAndEscapeProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI $projectIri"))

// Check user's permission for the operation
if (!requestingUser.isSystemAdmin
Expand Down Expand Up @@ -386,7 +386,7 @@ case class AdministrativePermissionForProjectGroupGetADM(projectIri: IRI, groupI
extends PermissionsResponderRequestADM {

implicit protected val stringFormatter: StringFormatter = StringFormatter.getInstanceForConstantOntologies
stringFormatter.validateProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI"))
stringFormatter.validateAndEscapeProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI $projectIri"))

// Check user's permission for the operation
if (!requestingUser.isSystemAdmin
Expand Down Expand Up @@ -488,7 +488,7 @@ case class DefaultObjectAccessPermissionsForProjectGetRequestADM(projectIri: IRI
extends PermissionsResponderRequestADM {

implicit protected val stringFormatter: StringFormatter = StringFormatter.getInstanceForConstantOntologies
stringFormatter.validateProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI"))
stringFormatter.validateAndEscapeProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI $projectIri"))

// Check user's permission for the operation
if (!requestingUser.isSystemAdmin
Expand Down Expand Up @@ -516,7 +516,7 @@ case class DefaultObjectAccessPermissionGetRequestADM(projectIri: IRI,
extends PermissionsResponderRequestADM {

implicit protected val stringFormatter: StringFormatter = StringFormatter.getInstanceForConstantOntologies
stringFormatter.validateProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI"))
stringFormatter.validateAndEscapeProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI $projectIri"))

// Check user's permission for the operation
if (!requestingUser.isSystemAdmin
Expand Down Expand Up @@ -592,7 +592,7 @@ case class DefaultObjectAccessPermissionsStringForResourceClassGetADM(projectIri
extends PermissionsResponderRequestADM {

implicit protected val stringFormatter: StringFormatter = StringFormatter.getInstanceForConstantOntologies
stringFormatter.validateProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI"))
stringFormatter.validateAndEscapeProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI $projectIri"))

// Check user's permission for the operation
if (!requestingUser.isSystemAdmin
Expand Down Expand Up @@ -628,7 +628,7 @@ case class DefaultObjectAccessPermissionsStringForPropertyGetADM(projectIri: IRI
extends PermissionsResponderRequestADM {

implicit protected val stringFormatter: StringFormatter = StringFormatter.getInstanceForConstantOntologies
stringFormatter.validateProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI"))
stringFormatter.validateAndEscapeProjectIri(projectIri, throw BadRequestException(s"Invalid project IRI $projectIri"))

// Check user's permission for the operation
if (!requestingUser.isSystemAdmin
Expand Down

0 comments on commit b3c2d5f

Please sign in to comment.