diff --git a/build.sbt b/build.sbt index 223396c787..446d24a44d 100644 --- a/build.sbt +++ b/build.sbt @@ -125,7 +125,7 @@ lazy val webapi: Project = Project(id = "webapi", base = file("webapi")) // add needed files to test jar Test / packageBin / mappings ++= Seq( (rootBaseDir.value / "webapi" / "scripts" / "fuseki-repository-config.ttl.template") -> "webapi/scripts/fuseki-repository-config.ttl.template", // needed for initialization of triplestore - (rootBaseDir.value / "sipi" / "config" / "sipi.knora-docker-config.lua") -> "sipi/config/sipi.knora-docker-config.lua" + (rootBaseDir.value / "sipi" / "config" / "sipi.docker-config.lua") -> "sipi/config/sipi.docker-config.lua" ), // use packaged jars (through packageBin) on classpaths instead of class directories for test Test / exportJars := true diff --git a/docker-compose.yml b/docker-compose.yml index 3df7a14509..c27cb86dca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,8 +33,10 @@ services: - SIPI_EXTERNAL_PORT=1024 - SIPI_WEBAPI_HOSTNAME=api - SIPI_WEBAPI_PORT=3333 + - KNORA_WEBAPI_KNORA_API_EXTERNAL_HOST=0.0.0.0 + - KNORA_WEBAPI_KNORA_API_EXTERNAL_PORT=3333 # entrypoint: [ "valgrind", "--leak-check=yes", "/sipi/sipi" ] ## uncomment to run SIPI under valgrind - command: --config=/sipi/config/sipi.knora-docker-config.lua + command: --config=/sipi/config/sipi.docker-config.lua api: image: daschswiss/knora-api:latest @@ -58,6 +60,8 @@ services: - KNORA_WEBAPI_CACHE_SERVICE_REDIS_HOST=redis - KNORA_WEBAPI_CACHE_SERVICE_REDIS_PORT=6379 - KNORA_WEBAPI_ALLOW_RELOAD_OVER_HTTP=true + - KNORA_WEBAPI_KNORA_API_EXTERNAL_HOST=0.0.0.0 + - KNORA_WEBAPI_KNORA_API_EXTERNAL_PORT=3333 networks: knora-net: diff --git a/docs/05-internals/design/api-v2/sipi.md b/docs/05-internals/design/api-v2/sipi.md index d3aad79b51..162fe3c1ee 100644 --- a/docs/05-internals/design/api-v2/sipi.md +++ b/docs/05-internals/design/api-v2/sipi.md @@ -3,24 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 --> -# Knora and Sipi +# DSP-API and Sipi ## Configuration -The Knora-specific configuration and scripts for Sipi are in the -`sipi` subdirectory of the Knora source tree. See the `README.md` there for -instructions on how to start Sipi with Knora. +The DSP-API specific configuration and scripts for Sipi are in the +`sipi` subdirectory of the DSP-API source tree. See the `README.md` for +instructions on how to start Sipi with DSP-API. ## Lua Scripts DSP-API v2 uses custom Lua scripts to control Sipi. These scripts can be -found in `sipi/scripts` in the Knora source tree. +found in `sipi/scripts` in the DSP-API source tree. Each of these scripts expects a [JSON Web Token](https://jwt.io/) in the -URL parameter `token`. In all cases, the token must be signed by Knora, -it must have an expiration date and not have expired, its issuer must be `Knora`, -and its audience must include `Sipi`. The other contents of the expected tokens -are described below. +URL parameter `token`. In all cases, the token must be signed by DSP-API, +it must have an expiration date and not have expired, its issuer must equal +the hostname and port of the API, and its audience must include `Sipi`. +The other contents of the expected tokens are described below. ### upload.lua @@ -53,7 +53,7 @@ must be a JSON object containing: ### delete_temp_file.lua The `delete_temp_file.lua` script is available at Sipi's `delete_temp_file` route. -It is used only if Knora rejects a file value update request. It expects an +It is used only if DSP-API rejects a file value update request. It expects an HTTP `DELETE` request, with a filename as the last component of the URL. The JWT sent to this script must contain the key `knora-data`, whose value @@ -64,7 +64,7 @@ must be a JSON object containing: ## SipiConnector -In Knora, the `org.knora.webapi.iiif.SipiConnector` handles all communication +In DSP-API, the `org.knora.webapi.iiif.SipiConnector` handles all communication with Sipi. It blocks while processing each request, to ensure that the number of concurrent requests to Sipi is not greater than `akka.actor.deployment./storeManager/iiifManager/sipiConnector.nr-of-instances`. @@ -76,7 +76,7 @@ If it encounters an error, it returns `SipiException`. `upload.lua`. The image is converted to JPEG 2000 and stored in Sipi's `tmp` directory. In the response, the client receives the JPEG 2000's unique, randomly generated filename. -2. The client submits a JSON-LD request to a Knora route (`/v2/values` or `/v2/resources`) +2. The client submits a JSON-LD request to a DSP-API route (`/v2/values` or `/v2/resources`) to create or change a file value. The request includes Sipi's internal filename. 3. During parsing of this JSON-LD request, a `StillImageFileValueContentV2` is constructed to represent the file value. During the construction of this @@ -93,14 +93,14 @@ If it encounters an error, it returns `SipiException`. `DeleteTemporaryFileRequestV2` to `SipiConnector`, which makes a request to Sipi's `delete_temp_file` route. -If the request to Knora cannot be parsed, the temporary file is not deleted +If the request to DSP-API cannot be parsed, the temporary file is not deleted immediately, but it will be deleted during the processing of a subsequent request by Sipi's `upload` route. -If Sipi's `store` route fails, Knora returns the `SipiException` to the client. +If Sipi's `store` route fails, DSP-API returns the `SipiException` to the client. In this case, manual intervention may be necessary to restore consistency -between Knora and Sipi. +between DSP-API and Sipi. If Sipi's `delete_temp_file` route fails, the error is not returned to the client, -because there is already a Knora error that needs to be returned to the client. +because there is already a DSP-API error that needs to be returned to the client. In this case, the Sipi error is simply logged. diff --git a/docs/05-internals/development/overview.md b/docs/05-internals/development/overview.md index f40e09948a..e933280f12 100644 --- a/docs/05-internals/development/overview.md +++ b/docs/05-internals/development/overview.md @@ -78,7 +78,7 @@ image in the background. The default behaviour is to start Sipi by calling the following command: ``` -$ /sipi/local/bin/sipi -config /sipi/config/sipi.knora-test-config.lua +$ /sipi/local/bin/sipi -config /sipi/config/sipi.test-config.lua ``` To override this default behaviour, start the container by supplying @@ -101,7 +101,7 @@ $ docker run --name sipi \ -p 1024:1024 \ -v $PWD:/localdir \ daschswiss/sipi \ - /sipi/local/bin/sipi -config /localdir/sipi.knora-test-config.lua + /sipi/local/bin/sipi -config /localdir/sipi.test-config.lua ``` ## Redis Server diff --git a/docs/07-sipi/index.md b/docs/07-sipi/index.md index 44a047fe13..29fafe2edb 100644 --- a/docs/07-sipi/index.md +++ b/docs/07-sipi/index.md @@ -9,8 +9,8 @@ for serving and converting binary media files such as images and video. Sipi can efficiently convert between many different formats on demand, preserving embedded metadata, and implements the [International Image -Interoperability Framework (IIIF)](http://iiif.io/). Knora is designed +Interoperability Framework (IIIF)](http://iiif.io/). DSP-API is designed to use Sipi for converting and serving media files. -* [Setting Up Sipi for Knora](setup-sipi-for-knora.md) -* [Interaction Between Sipi and Knora](sipi-and-knora.md) +* [Setting Up Sipi for DSP-API](setup-sipi-for-dsp-api.md) +* [Interaction Between Sipi and DSP-API](sipi-and-dsp-api.md) diff --git a/docs/07-sipi/setup-sipi-for-knora.md b/docs/07-sipi/setup-sipi-for-dsp-api.md similarity index 67% rename from docs/07-sipi/setup-sipi-for-knora.md rename to docs/07-sipi/setup-sipi-for-dsp-api.md index 8327fea2ba..22b456c2b9 100644 --- a/docs/07-sipi/setup-sipi-for-knora.md +++ b/docs/07-sipi/setup-sipi-for-dsp-api.md @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 --> -# Setting Up Sipi for Knora +# Setting Up Sipi for DSP-API ## Setup and Execution In order to serve files to the client application like the Salsah GUI, Sipi must be set up and running. Sipi can be downloaded from its own -GitHub repository: (which requires +GitHub repository: (which requires building from source), or the published [docker image](https://hub.docker.com/r/daschswiss/sipi/). can be used. To start Sipi, run the following command from inside the `sipi/` folder: @@ -17,51 +17,50 @@ folder: ``` $ export DOCKERHOST=LOCAL_IP_ADDRESS $ docker image rm --force daschswiss/sipi:main // deletes cached image and needs only to be used when newer image is available on dockerhub -$ docker run --rm -it --add-host webapihost:$DOCKERHOST -v $PWD/config:/sipi/config -v $PWD/scripts:/sipi/scripts -v /tmp:/tmp -v $HOME:$HOME -p 1024:1024 daschswiss/sipi:main --config=/sipi/config/sipi.knora-docker-config.lua +$ docker run --rm -it --add-host webapihost:$DOCKERHOST -v $PWD/config:/sipi/config -v $PWD/scripts:/sipi/scripts -v /tmp:/tmp -v $HOME:$HOME -p 1024:1024 daschswiss/sipi:main --config=/sipi/config/sipi.docker-config.lua ``` -where `LOCAL_IP_ADDRESS` is the IP of the host running the `Knora`. +where `LOCAL_IP_ADDRESS` is the IP of the host running `DSP-API`. -`--config=/sipi/config/sipi.knora-docker-config.lua` (or `--config=/sipi/config/sipi.knora-docker-it-config.lua` for -using sipi for integration testing). Please see `sipi.knora-docker-config.lua` for the settings like URL, port number -etc. These settings need to be set accordingly in Knora's `application.conf`. If you use the default settings both in -Sipi and Knora, there is no need to change these settings. +`--config=/sipi/config/sipi.docker-config.lua`. Please see `sipi.docker-config.lua` for the settings like URL, port number +etc. These settings need to be set according to DSP-API's `application.conf`. If you use the default settings both in +Sipi and DSP-API, there is no need to change these settings. Whenever a file is requested from Sipi (e.g. a browser trying to -dereference an image link served by Knora), a preflight function is -called. This function is defined in `sipi.init-knora.lua` present in the +dereference an image link served by DSP-API), a preflight function is +called. This function is defined in `sipi.init.lua` present in the Sipi root directory. It takes three parameters: `prefix`, `identifier` (the name of the requested file), and `cookie`. The prefix is the shortcode of the project that the resource containing the file value belongs to. -Given this information, Sipi asks Knora about the current's users -permissions on the given file. The cookie contains the current user's -Knora session id, so Knora can match Sipi's request with a given user +Given this information, Sipi asks the API about the current user's +permissions on the given file. The cookie contains the current user's +session id, so the API can match Sipi's request with a given user profile and determine the permissions this user has on the file. If the -Knora response grants sufficient permissions, the file is served in the -requested quality. If the suer has preview rights, Sipi serves a reduced +response grants sufficient permissions, the file is served in the +requested quality. If the user has preview rights, Sipi serves the file in reduced quality or integrates a watermark. If the user has no permissions, Sipi refuses to serve the file. However, all of this behaviour is defined in -the preflight function in Sipi and not controlled by Knora. Knora only +the preflight function in Sipi and not controlled by the API. DSP-API only provides the permission code. -See [Authentication of Users with Sipi](sipi-and-knora.md#authentication-of-users-with-sipi) for more +See [Authentication of Users with Sipi](sipi-and-dsp-api.md#authentication-of-users-with-sipi) for more information about sharing the session ID. ## Using Sipi in Test Mode -If you just want to test Sipi with Knora without serving the actual +If you just want to test Sipi with DSP-API without serving the actual files (e.g. when executing browser tests), you can simply start Sipi like this: ``` $ export DOCKERHOST=LOCAL_IP_ADDRESS $ docker image rm --force daschswiss/sipi:main // deletes cached image and needs only to be used when newer image is available on dockerhub -$ docker run --rm -it --add-host webapihost:$DOCKERHOST -v $PWD/config:/sipi/config -v $PWD/scripts:/sipi/scripts -v /tmp:/tmp -v $HOME:$HOME -p 1024:1024 daschswiss/sipi:main --config=/sipi/config/sipi.knora-docker-test-config.lua +$ docker run --rm -it --add-host webapihost:$DOCKERHOST -v $PWD/config:/sipi/config -v $PWD/scripts:/sipi/scripts -v /tmp:/tmp -v $HOME:$HOME -p 1024:1024 daschswiss/sipi:main --config=/sipi/config/sipi.docker-test-config.lua ``` -Then always the same test file will be served which is included in Sipi. In test mode, Sipi will -not ask Knora about the user's permission on the requested file. +Then always the same test file will be served which is delivered with Sipi. In test mode, Sipi will +not ask DSP-API about the user's permission on the requested file. ## Additional Sipi Environment Variables @@ -70,7 +69,7 @@ Additionally, these environment variables can be used to further configure Sipi: - `SIPI_WEBAPI_HOSTNAME=localhost`: overrides `knora_path` in Sipi's config - `SIPI_WEBAPI_PORT=3333`: overrides `knora_port` in Sipi's config -These variables need to be explicitly used like in `sipi.ini-knora.lua`: +These variables need to be explicitly used like in `sipi.init.lua`: ```lua -- diff --git a/docs/07-sipi/sipi-and-knora.md b/docs/07-sipi/sipi-and-dsp-api.md similarity index 79% rename from docs/07-sipi/sipi-and-knora.md rename to docs/07-sipi/sipi-and-dsp-api.md index b043d0073d..881a70beba 100644 --- a/docs/07-sipi/sipi-and-knora.md +++ b/docs/07-sipi/sipi-and-dsp-api.md @@ -3,29 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 --> -# Interaction Between Sipi and Knora +# Interaction Between Sipi and DSP-API ## General Remarks -Knora and Sipi (Simple Image Presentation Interface) are two -**complementary** software projects. Whereas Knora deals with data that +DSP-API and Sipi (Simple Image Presentation Interface) are two +**complementary** software projects. Whereas DSP-API deals with data that is written to and read from a triplestore (metadata and annotations), Sipi takes care of storing, converting and serving image files as well as other types of files such as audio, video, or documents (binary files it just stores and serves). -Knora and Sipi stick to a clear division of responsibility regarding -files: Knora knows about the names of files that are attached to +DSP-API and Sipi stick to a clear division of responsibility regarding +files: DSP-API knows about the names of files that are attached to resources as well as some metadata and is capable of creating the URLs for the client to request them from Sipi, but the whole handling of files (storing, naming, organization of the internal directory structure, format conversions, and serving) is taken care of by Sipi. -## Adding Files to Knora +## Adding Files to DSP A file is first uploaded to Sipi, then its metadata is submitted to -Knora. The implementation of this procedure is described in -[Knora and Sipi](../05-internals/design/api-v2/sipi.md). Instructions +DSP. The implementation of this procedure is described in +[DSP-API and Sipi](../05-internals/design/api-v2/sipi.md). Instructions for the client are given in [Creating File Values](../03-apis/api-v2/editing-values.md#creating-file-values) (for DSP-API v2) and in @@ -44,7 +44,7 @@ different IIIF URLs, e.g. at different resolutions. See the `knora-api` ontology ### File URLs in API v1 -In API v1, for each file value, Knora creates several Sipi URLs for accessing the file at different +In API v1, for each file value, DSP-API creates several Sipi URLs for accessing the file at different resolutions: ``` @@ -91,10 +91,10 @@ Sipi, obtaining the binary representation in the desired quality. ## Authentication of Users with Sipi -Whenever a file is requested, Sipi asks Knora about the current user's permissions on the given file. -This is achieved by sharing the Knora session cookie with Sipi. When the user logs in to Knora using his +Whenever a file is requested, Sipi asks the DSP-API about the current user's permissions on the given file. +This is achieved by sharing the session cookie with Sipi. When the user logs in to DSP using his browser (using either `V1` or `V2` authentication route), a session cookie containing a JWT token representing -the user is stored in the user's client. This session cookie is then read by Sipi and used to ask Knora for +the user is stored in the user's client. This session cookie is then read by Sipi and used to ask DSP-API for the user's image permissions. For the session cookie to be sent to Sipi, both the DSP-API and Sipi endpoints need to diff --git a/mkdocs.yml b/mkdocs.yml index 005b756bf9..fe6c57a6f4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -121,8 +121,8 @@ nav: # - Index: 06-salsah/index.md - SIPI: - Index: 07-sipi/index.md - - Setting Up Sipi for DSP-API: 07-sipi/setup-sipi-for-knora.md - - Interaction between Sipi and DSP-API: 07-sipi/sipi-and-knora.md + - Setting Up Sipi for DSP-API: 07-sipi/setup-sipi-for-dsp-api.md + - Interaction between Sipi and DSP-API: 07-sipi/sipi-and-dsp-api.md - Lucene: - Overview: 08-lucene/index.md - Lucene Query Parser Syntax: 08-lucene/lucene-query-parser-syntax.md diff --git a/webapi/src/test/resources/sipi.knora-docker-config.lua b/sipi/config/sipi.docker-config.lua similarity index 92% rename from webapi/src/test/resources/sipi.knora-docker-config.lua rename to sipi/config/sipi.docker-config.lua index c16b30ec7e..c8f08dd270 100644 --- a/webapi/src/test/resources/sipi.knora-docker-config.lua +++ b/sipi/config/sipi.docker-config.lua @@ -96,7 +96,7 @@ sipi = { -- -- Lua script which is executed on initialization of the Lua interpreter -- - initscript = '/sipi/scripts/sipi.init-knora.lua', + initscript = '/sipi/scripts/sipi.init.lua', -- -- path to the caching directory @@ -195,24 +195,6 @@ routes = { method = 'DELETE', route = '/delete_temp_file', script = 'delete_temp_file.lua' - }, - -- - -- additional routes used for testing. should not be defined in production. - -- - { - method = 'GET', - route = '/test_functions', - script = 'test_functions.lua' - }, - { - method = 'GET', - route = '/test_file_info', - script = 'test_file_info.lua' - }, - { - method = 'GET', - route = '/test_knora_session_cookie', - script = 'test_knora_session_cookie.lua' } } diff --git a/sipi/config/sipi.knora-docker-no-auth-config.lua b/sipi/config/sipi.docker-no-auth-config.lua similarity index 98% rename from sipi/config/sipi.knora-docker-no-auth-config.lua rename to sipi/config/sipi.docker-no-auth-config.lua index bad8d125e4..a7504f749a 100644 --- a/sipi/config/sipi.knora-docker-no-auth-config.lua +++ b/sipi/config/sipi.docker-no-auth-config.lua @@ -49,7 +49,7 @@ sipi = { -- -- Lua script which is executed on initialization of the Lua interpreter -- - initscript = '/sipi/scripts/sipi.init-knora-no-auth.lua', + initscript = '/sipi/scripts/sipi.init-no-auth.lua', -- -- path to the caching directory diff --git a/sipi/config/sipi.knora-docker-test-config.lua b/sipi/config/sipi.docker-test-config.lua similarity index 99% rename from sipi/config/sipi.knora-docker-test-config.lua rename to sipi/config/sipi.docker-test-config.lua index ecd46144a1..a89c455b0d 100644 --- a/sipi/config/sipi.knora-docker-test-config.lua +++ b/sipi/config/sipi.docker-test-config.lua @@ -68,7 +68,7 @@ sipi = { -- -- Lua script which is executed on initialization of the Lua interpreter -- - initscript = './scripts/sipi.init-knora-test.lua', + initscript = './scripts/sipi.init-test.lua', -- -- path to the caching directory diff --git a/sipi/config/sipi.knora-local-config.lua b/sipi/config/sipi.local-config.lua similarity index 98% rename from sipi/config/sipi.knora-local-config.lua rename to sipi/config/sipi.local-config.lua index c01f18758f..e7a0eed25e 100644 --- a/sipi/config/sipi.knora-local-config.lua +++ b/sipi/config/sipi.local-config.lua @@ -68,7 +68,7 @@ sipi = { -- -- Lua script which is executed on initialization of the Lua interpreter -- - initscript = './scripts/sipi.init-knora.lua', + initscript = './scripts/sipi.init.lua', -- -- path to the caching directory @@ -109,7 +109,7 @@ sipi = { -- -- Path to Knora Application -- - knora_path = 'localhost', + knora_path = '0.0.0.0', -- -- Port of Knora Application diff --git a/sipi/scripts/jwt.lua b/sipi/scripts/jwt.lua index 2a5ef9b2ca..9f887b5043 100644 --- a/sipi/scripts/jwt.lua +++ b/sipi/scripts/jwt.lua @@ -14,8 +14,22 @@ function get_knora_token() return nil end - if token["iss"] ~= "Knora" then - send_error(401, "Not a Knora token") + local webapi_hostname = os.getenv("KNORA_WEBAPI_KNORA_API_EXTERNAL_HOST") + local webapi_port = os.getenv("KNORA_WEBAPI_KNORA_API_EXTERNAL_PORT") + if webapi_hostname == nil then + send_error(500, "KNORA_WEBAPI_KNORA_API_EXTERNAL_HOST not set") + return nil + end + if webapi_port == nil then + send_error(500, "KNORA_WEBAPI_KNORA_API_EXTERNAL_PORT not set") + return nil + end + + token_issuer = webapi_hostname .. ':' .. webapi_port + server.log("token_issuer:" .. token_issuer, server.loglevel.LOG_DEBUG) + if token["iss"] ~= token_issuer then + server.log(token_issuer, server.loglevel.LOG_DEBUG) + send_error(401, "Invalid token. The token was not issued by the same server that sent the request.") return nil end diff --git a/sipi/scripts/sipi.init-knora-test.lua b/sipi/scripts/sipi.init-knora-test.lua deleted file mode 100644 index d570532d2a..0000000000 --- a/sipi/scripts/sipi.init-knora-test.lua +++ /dev/null @@ -1,128 +0,0 @@ --- * Copyright © 2021 - 2022 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. --- * SPDX-License-Identifier: Apache-2.0 - -require "get_knora_session" - -------------------------------------------------------------------------------- --- This function is being called from sipi before the file is served --- Knora is called to ask for the user's permissions on the file --- Parameters: --- prefix: This is the prefix that is given on the IIIF url --- identifier: the identifier for the image --- cookie: The cookie that may be present --- --- Returns: --- permission: --- 'allow' : the view is allowed with the given IIIF parameters --- 'restrict:watermark=' : Add a watermark --- 'restrict:size=' : reduce size/resolution --- 'deny' : no access! --- filepath: server-path where the master file is located -------------------------------------------------------------------------------- -function pre_flight(prefix,identifier,cookie) - server.log("pre_flight called in sipi.init-knora-test.lua", server.loglevel.LOG_DEBUG) - - - -- - -- For Knora Sipi integration testing - -- Always the same test file is served - -- Make sure that this image file exists in config.imgroot - -- - - if config.prefix_as_path then - filepath = config.imgroot .. '/' .. prefix .. '/' .. 'Leaves.jp2' - else - filepath = config.imgroot .. '/' .. 'Leaves.jp2' - end - - if prefix == "thumbs" then - -- always allow thumbnails - return 'allow', filepath - end - - if prefix == "tmp" then - -- always allow access to tmp folder - return 'allow', filepath - end - - knora_cookie_header = nil - - if cookie ~='' then - - -- tries to extract the Knora session id from the cookie: - -- gets the digits between "sid=" and the closing ";" (only given in case of several key value pairs) - -- returns nil if it cannot find it - session_id = get_session_id(cookie) - - if session_id == nil then - -- no session_id could be extracted - print("cookie key is invalid: " .. cookie) - else - knora_cookie_header = { Cookie = "KnoraAuthentication=" .. session_id } - end - end - - knora_url = 'http://' .. config.knora_path .. ':' .. config.knora_port .. '/admin/files/' .. prefix .. '/' .. identifier - - --print("knora_url: " .. knora_url) - - success, result = server.http("GET", knora_url, knora_cookie_header, 5000) - - -- check HTTP request was successful - if not success then - server.log("Server.http() failed: " .. result, server.loglevel.LOG_ERR) - return 'deny' - end - - if result.status_code ~= 200 then - server.log("Knora returned HTTP status code " .. result.status_code) - server.log(result.body) - return 'deny' - end - - success, response_json = server.json_to_table(result.body) - if not success then - server.log("Server.http() failed: " .. response_json, server.loglevel.LOG_ERR) - return 'deny' - end - - server.log("pre_flight - permission code: " .. response_json.permissionCode, server.loglevel.LOG_DEBUG) - - if response_json.permissionCode == 0 then - -- no view permission on file - return 'deny' - elseif response_json.permissionCode == 1 then - -- restricted view permission on file - -- either watermark or size (depends on project, should be returned with permission code by Sipi responder) - -- currently, only size is used - - local restrictedViewSize - - if response_json.restrictedViewSettings ~= nil then - -- server.log("pre_flight - restricted view settings - watermark: " .. tostring(response_json.restrictedViewSettings.watermark), server.loglevel.LOG_DEBUG) - - if response_json.restrictedViewSettings.size ~= nil then - server.log("pre_flight - restricted view settings - size: " .. tostring(response_json.restrictedViewSettings.size), server.loglevel.LOG_DEBUG) - restrictedViewSize = response_json.restrictedViewSettings.size - else - server.log("pre_flight - using default restricted view size", server.loglevel.LOG_DEBUG) - restrictedViewSize = config.thumb_size - end - else - server.log("pre_flight - using default restricted view size", server.loglevel.LOG_DEBUG) - restrictedViewSize = config.thumb_size - end - - return { - type = 'restrict', - size = restrictedViewSize - }, filepath - elseif response_json.permissionCode >= 2 then - -- full view permissions on file - return 'allow', filepath - else - -- invalid permission code - return 'deny' - end -end -------------------------------------------------------------------------------- diff --git a/sipi/scripts/sipi.init-knora-no-auth.lua b/sipi/scripts/sipi.init-no-auth.lua similarity index 93% rename from sipi/scripts/sipi.init-knora-no-auth.lua rename to sipi/scripts/sipi.init-no-auth.lua index 981965dd36..d02194fd16 100644 --- a/sipi/scripts/sipi.init-knora-no-auth.lua +++ b/sipi/scripts/sipi.init-no-auth.lua @@ -20,7 +20,7 @@ require "get_knora_session" -- filepath: server-path where the master file is located ------------------------------------------------------------------------------- function pre_flight(prefix,identifier,cookie) - server.log("pre_flight called in sipi.init-knora-no-auth.lua", server.loglevel.LOG_DEBUG) + server.log("pre_flight called in sipi.init-no-auth.lua", server.loglevel.LOG_DEBUG) -- -- Allways allows access to images. No authorization from Knora is retrieved diff --git a/sipi/scripts/sipi.init-knora.lua b/sipi/scripts/sipi.init.lua similarity index 98% rename from sipi/scripts/sipi.init-knora.lua rename to sipi/scripts/sipi.init.lua index 5555efcfcc..8334197ff2 100644 --- a/sipi/scripts/sipi.init-knora.lua +++ b/sipi/scripts/sipi.init.lua @@ -20,7 +20,7 @@ require "get_knora_session" -- filepath: server-path where the master file is located ------------------------------------------------------------------------------- function pre_flight(prefix, identifier, cookie) - server.log("pre_flight called in sipi.init-knora.lua", server.loglevel.LOG_DEBUG) + server.log("pre_flight called in sipi.init.lua", server.loglevel.LOG_DEBUG) if config.prefix_as_path then filepath = config.imgroot .. '/' .. prefix .. '/' .. identifier diff --git a/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala b/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala index f2025621de..6b6de7d274 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/Authenticator.scala @@ -37,6 +37,7 @@ import spray.json._ import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success, Try} +import org.knora.webapi.settings.KnoraSettingsImpl /** * This trait is used in routes that need authentication support. It provides methods that use the [[RequestContext]] @@ -84,7 +85,8 @@ trait Authenticator extends InstrumentationSupport { sessionToken = JWTHelper.createToken( userProfile.userData.user_id.get, settings.jwtSecretKey, - settings.jwtLongevity + settings.jwtLongevity, + settings.externalKnoraApiHostPort ) httpResponse = HttpResponse( @@ -145,7 +147,12 @@ trait Authenticator extends InstrumentationSupport { ) cookieDomain = Some(settings.cookieDomain) - token = JWTHelper.createToken(userADM.id, settings.jwtSecretKey, settings.jwtLongevity) + token = JWTHelper.createToken( + userADM.id, + settings.jwtSecretKey, + settings.jwtLongevity, + settings.externalKnoraApiHostPort + ) httpResponse = HttpResponse( headers = List( @@ -538,13 +545,13 @@ object Authenticator extends InstrumentationSupport { } } yield true case Some(KnoraJWTTokenCredentialsV2(jwtToken)) => - if (!JWTHelper.validateToken(jwtToken, settings.jwtSecretKey)) { + if (!JWTHelper.validateToken(jwtToken, settings.jwtSecretKey, settings.externalKnoraApiHostPort)) { log.debug("authenticateCredentialsV2 - token was not valid") throw BadCredentialsException(BAD_CRED_NOT_VALID) } FastFuture.successful(true) case Some(KnoraSessionCredentialsV2(sessionToken)) => - if (!JWTHelper.validateToken(sessionToken, settings.jwtSecretKey)) { + if (!JWTHelper.validateToken(sessionToken, settings.jwtSecretKey, settings.externalKnoraApiHostPort)) { log.debug("authenticateCredentialsV2 - session token was not valid") throw BadCredentialsException(BAD_CRED_NOT_VALID) } @@ -756,7 +763,11 @@ object Authenticator extends InstrumentationSupport { featureFactoryConfig = featureFactoryConfig ) case Some(KnoraJWTTokenCredentialsV2(jwtToken)) => - val userIri: IRI = JWTHelper.extractUserIriFromToken(jwtToken, settings.jwtSecretKey) match { + val userIri: IRI = JWTHelper.extractUserIriFromToken( + jwtToken, + settings.jwtSecretKey, + settings.externalKnoraApiHostPort + ) match { case Some(iri) => iri case None => // should not happen, as the token is already validated @@ -768,7 +779,11 @@ object Authenticator extends InstrumentationSupport { featureFactoryConfig = featureFactoryConfig ) case Some(KnoraSessionCredentialsV2(sessionToken)) => - val userIri: IRI = JWTHelper.extractUserIriFromToken(sessionToken, settings.jwtSecretKey) match { + val userIri: IRI = JWTHelper.extractUserIriFromToken( + sessionToken, + settings.jwtSecretKey, + settings.externalKnoraApiHostPort + ) match { case Some(iri) => iri case None => // should not happen, as the token is already validated @@ -846,6 +861,7 @@ object JWTHelper { * @param userIri the user IRI that will be encoded into the token. * @param secret the secret key used for encoding. * @param longevity the token's longevity. + * @param issuer the principal that issued the JWT. * @param content any other content to be included in the token. * @return a [[String]] containing the JWT. */ @@ -853,6 +869,7 @@ object JWTHelper { userIri: IRI, secret: String, longevity: FiniteDuration, + issuer: String, content: Map[String, JsValue] = Map.empty ): String = { val stringFormatter = StringFormatter.getGeneralInstance @@ -867,7 +884,7 @@ object JWTHelper { val claim: String = JwtClaim( content = JsObject(content).compactPrint, - issuer = Some("Knora"), + issuer = Some(issuer), subject = Some(userIri), audience = Some(Set("Knora", "Sipi")), issuedAt = Some(now), @@ -890,15 +907,16 @@ object JWTHelper { * * @param token the JWT. * @param secret the secret used to encode the token. + * @param issuer the principal that issued the JWT. * @return a [[Boolean]]. */ - def validateToken(token: String, secret: String): Boolean = + def validateToken(token: String, secret: String, issuer: String): Boolean = if (CacheUtil.get[UserADM](AUTHENTICATION_INVALIDATION_CACHE_NAME, token).nonEmpty) { // token invalidated so no need to decode log.debug("validateToken - token found in invalidation cache, so not valid") false } else { - decodeToken(token, secret).isDefined + decodeToken(token, secret, issuer).isDefined } /** @@ -906,10 +924,11 @@ object JWTHelper { * * @param token the JWT. * @param secret the secret used to encode the token. + * @param issuer the principal that issued the JWT. * @return an optional [[IRI]]. */ - def extractUserIriFromToken(token: String, secret: String): Option[IRI] = - decodeToken(token, secret) match { + def extractUserIriFromToken(token: String, secret: String, issuer: String): Option[IRI] = + decodeToken(token, secret, issuer) match { case Some((_: JwtHeader, claim: JwtClaim)) => claim.subject case None => None } @@ -921,10 +940,11 @@ object JWTHelper { * @param token the JWT. * @param secret the secret used to encode the token. * @param contentName the name of the content field to be extracted. + * @param issuer the principal that issued the JWT. * @return the string value of the specified content field. */ - def extractContentFromToken(token: String, secret: String, contentName: String): Option[String] = - decodeToken(token, secret) match { + def extractContentFromToken(token: String, secret: String, contentName: String, issuer: String): Option[String] = + decodeToken(token, secret, issuer) match { case Some((_: JwtHeader, claim: JwtClaim)) => claim.content.parseJson.asJsObject.fields.get(contentName) match { case Some(jsString: JsString) => Some(jsString.value) @@ -941,14 +961,14 @@ object JWTHelper { * @param secret the secret used to encode the token. * @return the token's header and claim, or `None` if the token is invalid. */ - private def decodeToken(token: String, secret: String): Option[(JwtHeader, JwtClaim)] = { + private def decodeToken(token: String, secret: String, issuer: String): Option[(JwtHeader, JwtClaim)] = { implicit val stringFormatter: StringFormatter = StringFormatter.getGeneralInstance JwtSprayJson.decodeAll(token, secret, Seq(JwtAlgorithm.HS256)) match { case Success((header: JwtHeader, claim: JwtClaim, _)) => val missingRequiredContent: Boolean = Set( header.typ.isDefined, - claim.issuer.isDefined, + claim.issuer.isDefined && claim.issuer.contains(issuer), claim.subject.isDefined, claim.jwtId.isDefined, claim.issuedAt.isDefined, diff --git a/webapi/src/main/scala/org/knora/webapi/store/iiif/SipiConnector.scala b/webapi/src/main/scala/org/knora/webapi/store/iiif/SipiConnector.scala index 070a896c1c..26ec305bec 100644 --- a/webapi/src/main/scala/org/knora/webapi/store/iiif/SipiConnector.scala +++ b/webapi/src/main/scala/org/knora/webapi/store/iiif/SipiConnector.scala @@ -171,6 +171,7 @@ class SipiConnector extends Actor with ActorLogging { userIri = moveTemporaryFileToPermanentStorageRequestV2.requestingUser.id, secret = settings.jwtSecretKey, longevity = settings.jwtLongevity, + issuer = settings.externalKnoraApiHostPort, content = Map( "knora-data" -> JsObject( Map( @@ -207,6 +208,7 @@ class SipiConnector extends Actor with ActorLogging { userIri = deleteTemporaryFileRequestV2.requestingUser.id, secret = settings.jwtSecretKey, longevity = settings.jwtLongevity, + settings.externalKnoraApiHostPort, content = Map( "knora-data" -> JsObject( Map( diff --git a/sipi/config/sipi.knora-docker-config.lua b/webapi/src/test/resources/sipi.docker-config.lua similarity index 92% rename from sipi/config/sipi.knora-docker-config.lua rename to webapi/src/test/resources/sipi.docker-config.lua index c16b30ec7e..c8f08dd270 100644 --- a/sipi/config/sipi.knora-docker-config.lua +++ b/webapi/src/test/resources/sipi.docker-config.lua @@ -96,7 +96,7 @@ sipi = { -- -- Lua script which is executed on initialization of the Lua interpreter -- - initscript = '/sipi/scripts/sipi.init-knora.lua', + initscript = '/sipi/scripts/sipi.init.lua', -- -- path to the caching directory @@ -195,24 +195,6 @@ routes = { method = 'DELETE', route = '/delete_temp_file', script = 'delete_temp_file.lua' - }, - -- - -- additional routes used for testing. should not be defined in production. - -- - { - method = 'GET', - route = '/test_functions', - script = 'test_functions.lua' - }, - { - method = 'GET', - route = '/test_file_info', - script = 'test_file_info.lua' - }, - { - method = 'GET', - route = '/test_knora_session_cookie', - script = 'test_knora_session_cookie.lua' } } diff --git a/webapi/src/test/scala/org/knora/webapi/TestContainersAll.scala b/webapi/src/test/scala/org/knora/webapi/TestContainersAll.scala index 0d5d3fc905..4514dc36d8 100644 --- a/webapi/src/test/scala/org/knora/webapi/TestContainersAll.scala +++ b/webapi/src/test/scala/org/knora/webapi/TestContainersAll.scala @@ -36,18 +36,21 @@ object TestContainersAll { val SipiImageName: DockerImageName = DockerImageName.parse(s"daschswiss/knora-sipi:${BuildInfo.version}") val SipiContainer = new GenericContainer(SipiImageName) SipiContainer.withExposedPorts(1024) + SipiContainer.withEnv("KNORA_WEBAPI_KNORA_API_EXTERNAL_HOST", "0.0.0.0") + SipiContainer.withEnv("KNORA_WEBAPI_KNORA_API_EXTERNAL_PORT", "3333") SipiContainer.withEnv("SIPI_EXTERNAL_PROTOCOL", "http") SipiContainer.withEnv("SIPI_EXTERNAL_HOSTNAME", "0.0.0.0") SipiContainer.withEnv("SIPI_EXTERNAL_PORT", "1024") SipiContainer.withEnv("SIPI_WEBAPI_HOSTNAME", localIpAddress) SipiContainer.withEnv("SIPI_WEBAPI_PORT", "3333") - SipiContainer.withCommand("--config=/sipi/config/sipi.knora-docker-config.lua") + + SipiContainer.withCommand("--config=/sipi/config/sipi.docker-config.lua") // TODO: Needs https://github.com/scalameta/metals/issues/3623 to be resolved SipiContainer.withClasspathResourceMapping( - // "/sipi/config/sipi.knora-docker-config.lua" - "/sipi.knora-docker-config.lua", - "/sipi/config/sipi.knora-docker-config.lua", + // "/sipi/config/sipi.docker-config.lua" + "/sipi.docker-config.lua", + "/sipi/config/sipi.docker-config.lua", BindMode.READ_ONLY ) SipiContainer.start() diff --git a/webapi/src/test/scala/org/knora/webapi/routing/AuthenticatorSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/AuthenticatorSpec.scala index d9ead62156..54dd130962 100644 --- a/webapi/src/test/scala/org/knora/webapi/routing/AuthenticatorSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/routing/AuthenticatorSpec.scala @@ -131,7 +131,12 @@ class AuthenticatorSpec extends CoreSpec("AuthenticationTestSystem") with Implic } } "succeed with correct token" in { - val token = JWTHelper.createToken("myuseriri", settings.jwtSecretKey, settings.jwtLongevity) + val token = JWTHelper.createToken( + "myuseriri", + settings.jwtSecretKey, + settings.jwtLongevity, + settings.externalKnoraApiHostPort + ) val tokenCreds = KnoraJWTTokenCredentialsV2(token) val resF = Authenticator invokePrivate authenticateCredentialsV2( Some(tokenCreds), @@ -145,7 +150,12 @@ class AuthenticatorSpec extends CoreSpec("AuthenticationTestSystem") with Implic } } "fail with invalidated token" in { - val token = JWTHelper.createToken("myuseriri", settings.jwtSecretKey, settings.jwtLongevity) + val token = JWTHelper.createToken( + "myuseriri", + settings.jwtSecretKey, + settings.jwtLongevity, + settings.externalKnoraApiHostPort + ) val tokenCreds = KnoraJWTTokenCredentialsV2(token) CacheUtil.put(AUTHENTICATION_INVALIDATION_CACHE_NAME, tokenCreds.jwtToken, tokenCreds.jwtToken) val resF = Authenticator invokePrivate authenticateCredentialsV2( diff --git a/webapi/src/test/scala/org/knora/webapi/routing/JWTHelperSpec.scala b/webapi/src/test/scala/org/knora/webapi/routing/JWTHelperSpec.scala index 57b0ad96d4..579f0fd399 100644 --- a/webapi/src/test/scala/org/knora/webapi/routing/JWTHelperSpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/routing/JWTHelperSpec.scala @@ -25,7 +25,7 @@ object JWTHelperSpec { class JWTHelperSpec extends CoreSpec(JWTHelperSpec.config) with ImplicitSender { private val validToken: String = - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJLbm9yYSIsInN1YiI6Imh0dHA6Ly9yZGZoLmNoL3VzZXJzLzlYQkNyRFYzU1JhN2tTMVd3eW5CNFEiLCJhdWQiOlsiS25vcmEiLCJTaXBpIl0sImV4cCI6NDY5NTE5MzYwNSwiaWF0IjoxNTQxNTkzNjA1LCJqdGkiOiJsZmdreWJqRlM5Q1NiV19NeVA0SGV3IiwiZm9vIjoiYmFyIn0.qPMJjv8tVOM7KKDxR4Dmdz_kB0FzTOtJBYHSp62Dilk" + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIwLjAuMC4wOjMzMzMiLCJzdWIiOiJodHRwOi8vcmRmaC5jaC91c2Vycy85WEJDckRWM1NSYTdrUzFXd3luQjRRIiwiYXVkIjpbIktub3JhIiwiU2lwaSJdLCJleHAiOjQ4MDE0Njg1MTEsImlhdCI6MTY0Nzg2ODUxMSwianRpIjoiYXVVVUh1aDlUanF2SnBYUXVuOVVfZyIsImZvbyI6ImJhciJ9.6yHse3pNGdDqkC4PXdkm2ZtRqITqSwo0gvCZ__4jzHQ" "The JWTHelper" should { @@ -34,32 +34,37 @@ class JWTHelperSpec extends CoreSpec(JWTHelperSpec.config) with ImplicitSender { userIri = SharedTestDataADM.anythingUser1.id, secret = settings.jwtSecretKey, longevity = settings.jwtLongevity, + issuer = settings.externalKnoraApiHostPort, content = Map("foo" -> JsString("bar")) ) JWTHelper.extractUserIriFromToken( token = token, - secret = settings.jwtSecretKey + secret = settings.jwtSecretKey, + issuer = settings.externalKnoraApiHostPort ) should be(Some(SharedTestDataADM.anythingUser1.id)) JWTHelper.extractContentFromToken( token = token, secret = settings.jwtSecretKey, - contentName = "foo" + contentName = "foo", + issuer = settings.externalKnoraApiHostPort ) should be(Some("bar")) } "validate a token" in { JWTHelper.validateToken( token = validToken, - secret = settings.jwtSecretKey + secret = settings.jwtSecretKey, + issuer = settings.externalKnoraApiHostPort ) should be(true) } "extract the user's IRI" in { JWTHelper.extractUserIriFromToken( token = validToken, - secret = settings.jwtSecretKey + secret = settings.jwtSecretKey, + issuer = settings.externalKnoraApiHostPort ) should be(Some(SharedTestDataADM.anythingUser1.id)) } @@ -67,7 +72,8 @@ class JWTHelperSpec extends CoreSpec(JWTHelperSpec.config) with ImplicitSender { JWTHelper.extractContentFromToken( token = validToken, secret = settings.jwtSecretKey, - contentName = "foo" + contentName = "foo", + issuer = settings.externalKnoraApiHostPort ) should be(Some("bar")) } @@ -77,7 +83,8 @@ class JWTHelperSpec extends CoreSpec(JWTHelperSpec.config) with ImplicitSender { JWTHelper.extractUserIriFromToken( token = invalidToken, - secret = settings.jwtSecretKey + secret = settings.jwtSecretKey, + issuer = settings.externalKnoraApiHostPort ) should be(None) } @@ -87,7 +94,8 @@ class JWTHelperSpec extends CoreSpec(JWTHelperSpec.config) with ImplicitSender { JWTHelper.extractUserIriFromToken( token = tokenWithInvalidSubject, - secret = settings.jwtSecretKey + secret = settings.jwtSecretKey, + issuer = settings.externalKnoraApiHostPort ) should be(None) } @@ -97,7 +105,8 @@ class JWTHelperSpec extends CoreSpec(JWTHelperSpec.config) with ImplicitSender { JWTHelper.extractUserIriFromToken( token = tokenWithMissingExp, - secret = settings.jwtSecretKey + secret = settings.jwtSecretKey, + issuer = settings.externalKnoraApiHostPort ) should be(None) } @@ -107,8 +116,20 @@ class JWTHelperSpec extends CoreSpec(JWTHelperSpec.config) with ImplicitSender { JWTHelper.extractUserIriFromToken( token = expiredToken, - secret = settings.jwtSecretKey + secret = settings.jwtSecretKey, + issuer = settings.externalKnoraApiHostPort ) should be(None) } + + "reject a token with a different issuer than the one who created the token" in { + val tokenWithDifferentIssuer = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJibGFibGEiLCJzdWIiOiJodHRwOi8vcmRmaC5jaC91c2Vycy85WEJDckRWM1NSYTdrUzFXd3luQjRRIiwiYXVkIjpbIktub3JhIiwiU2lwaSJdLCJleHAiOjQ4MDE0NjkyNzgsImlhdCI6MTY0Nzg2OTI3OCwianRpIjoiYU9HRExCYnJUbi1iQUIwVXZzTDZMZyIsImZvbyI6ImJhciJ9.ewFp0uXjPkn6GSGvDcph1MZRPpip669IrpXQ8Qv3Vpw" + + JWTHelper.validateToken( + token = tokenWithDifferentIssuer, + secret = settings.jwtSecretKey, + issuer = settings.externalKnoraApiHostPort + ) should be(false) + } } }