Skip to content
Merged
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
3 changes: 2 additions & 1 deletion components/ILIAS/LTI/PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ In Administration > LTI, the privacy-related settings can be configured.
- For LTI Advantage tools, the Names and Role Provisioning Services can be activated (“Names and Role Provisioning Services”). A tool then can read the members of the course or group an object of it is in, with their roles (instructor or learner) and their status (active or inactive). Each member is described as a launch of the object describes the user, under the same privacy settings: user id, name and email only as far as “User identification” and “User name” allow. With a random user id per object, only the members who launched the object are given to the tool. ILIAS does not store this list; it is sent to the tool on request.
- An LTI Advantage tool can be registered through Dynamic Registration. ILIAS and the tool exchange their configuration (URLs, keys, client id, the scopes and messages the tool uses); no personal data is part of it. ILIAS stores the tool as an own tool of the user who registered it, as if that user had entered it.
- When a tool supports Deep Linking, the user who creates an LTI object picks its content in the tool. The request describes the user as a launch does, under the same privacy settings, with the role of an instructor. Each resource link the tool sends back becomes an object with the title, description and URL the tool gave; they hold no personal data.
- While a launch, a Deep Linking request or a Dynamic Registration of an LTI Advantage tool waits for the tool to answer, ILIAS keeps it for the session of the user in the table `lti_pending_requests`. For a launch and a Deep Linking request this is the message for the tool, which describes the user as set out above; the session is only stored as a hash. A request is deleted when the tool answers it, and an expired one (after at most an hour) with the next request of the same kind.
- If the provider or tool supports xAPI statements, refer to the PRIVACY.md file for xAPI. Typically, xAPI generates and transmits large amounts of behavioral data, often highly personalized. Proper privacy considerations must be taken into account when using xAPI.

### Data being stored by ILIAS as LTI Tool (Provider)
Expand Down Expand Up @@ -73,7 +74,7 @@ The log of the component `lti` names the users of each launch and score by their

### Data being deleted

- As LTI Tool (Consumer), the scores and results of the users of an object are deleted with the object when it is deleted from trash. Personal data transferred to a tool can only be deleted within that tool; how much is transferred depends on the privacy settings described above.
- As LTI Tool (Consumer), the scores and results of the users of an object are deleted with the object when it is deleted from trash. The requests waiting for a tool to answer are deleted when it answers them, or with the next request of the same kind once they expired. Personal data transferred to a tool can only be deleted within that tool; how much is transferred depends on the privacy settings described above.
- As LTI Tool (Provider), the accounts created for the users of a platform are deleted like any other account, in Administration > Users and Roles. Deleting a platform in Administration > LTI deletes its registration and the releases of objects to it, not the accounts.

### Data being exported
Expand Down
10 changes: 6 additions & 4 deletions components/ILIAS/LTI/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ LTI/
│ │ ├── Consumer/
│ │ └── Provider/
│ ├── LTIAdvantage/ LTI Advantage (LTI 1.3 and its services)
│ │ ├── Common/ Key pair, responses of the endpoints, requests waiting in the session
│ │ ├── Common/ Key pair, responses of the endpoints, requests a session waits for
│ │ ├── Platform/ ILIAS launches tools: Launch, DynamicRegistration, DeepLinking, AGS, NRPS
│ │ └── Tool/ Platforms launch ILIAS: DynamicRegistration, DeepLinking, NRPS
│ └── Setup/ Setup agent and database update steps
Expand Down Expand Up @@ -103,7 +103,7 @@ the endpoints:
* **ILIAS as platform:** `ilLTIAdvantagePlatformConnection` sets up the library for one tool: ILIAS is the
platform, with the client id it gave the tool and the id of the tool as deployment id, and the tool is the
default tool of the library, with its PEM key or key set URL. A launch starts the OpenID Connect login at
the tool, which gets the id_token from `ltiauth.php`; the login waits in the session and is used once.
the tool, which gets the id_token from `ltiauth.php`; the login waits for the session and is used once.
Tools often ask `ltiauth.php` with a POST from their own site, which does not carry a SameSite=Lax cookie,
so the page that starts the launch sends the session cookie again as SameSite=None, as `ilLTILoginGUI`
does for LTI sessions. An embedded launch also offers the platform storage of LTI (`Platform::getStorageJS()`
Expand All @@ -119,8 +119,10 @@ the endpoints:
URLs of `ltiservices.php` come from the configured HTTP path, because under that script the base URL of the
request ends in the script and its path. `ilLTIResponse` ends the requests of the endpoints, of the pages
that post a message to the other side and of the modals the tables load asynchronously, and
`ilLTIAdvantagePendingRequests` keeps in the session what waits to be answered: a login, a Deep Linking
request, a Dynamic Registration.
`ilLTIAdvantagePendingRequests` keeps for the session what waits to be answered: a login, a Deep Linking
request, a Dynamic Registration. They are kept in the database (`lti_pending_requests`), one row each, and
not in the session itself: ILIAS writes the whole session back when a request ends, so a request running in
parallel, like the asynchronous ones of the main bar during an embedded launch, would remove them again.
* **ILIAS as tool:** `lti.php` takes the OpenID Connect login and the id_token of a platform as well as an
LTI 1.1 launch. `ilLTIDataConnector` finds the platform by issuer, client id and deployment id, keeps the
key the library fetched from the key set of the platform and the access tokens of its services. The
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*
*********************************************************************/

declare(strict_types=1);

/**
* Keeps the pending requests of LTI Advantage in the database instead of the session: every request of ILIAS
* writes the whole session back when it ends, so a request that ran in parallel, like the asynchronous ones
* of the main bar, would remove a request kept in the session by another one in the meantime.
*
* @author Saúl Díaz <sdiaz@surlabs.com>
*/
final readonly class ilLTIAdvantagePendingRequestDBStorage implements ilLTIAdvantagePendingRequestStorage
{
public const string TABLE = 'lti_pending_requests';

public function __construct(private ilDBInterface $db)
{
}

public function read(string $session, string $kind, string $key): ?array
{
$result = $this->db->queryF(
'SELECT request_data, created FROM ' . self::TABLE . ' WHERE session_id = %s AND kind = %s AND request_key = %s',
[ilDBConstants::T_TEXT, ilDBConstants::T_TEXT, ilDBConstants::T_TEXT],
[$session, $kind, $key]
);
$row = $this->db->fetchAssoc($result);
if ($row === null) {
return null;
}
$request = json_decode((string) $row['request_data'], true);

return is_array($request) ? [$request, (int) $row['created']] : null;
}

public function write(string $session, string $kind, string $key, array $request, int $created): void
{
$this->db->replace(
self::TABLE,
[
'session_id' => [ilDBConstants::T_TEXT, $session],
'kind' => [ilDBConstants::T_TEXT, $kind],
'request_key' => [ilDBConstants::T_TEXT, $key],
],
[
'request_data' => [
ilDBConstants::T_CLOB,
json_encode($request, JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE)
],
'created' => [ilDBConstants::T_INTEGER, $created],
]
);
}

public function delete(string $session, string $kind, string $key): bool
{
return $this->db->manipulateF(
'DELETE FROM ' . self::TABLE . ' WHERE session_id = %s AND kind = %s AND request_key = %s',
[ilDBConstants::T_TEXT, ilDBConstants::T_TEXT, ilDBConstants::T_TEXT],
[$session, $kind, $key]
) > 0;
}

public function deleteOlderThan(string $kind, int $created): void
{
$this->db->manipulateF(
'DELETE FROM ' . self::TABLE . ' WHERE kind = %s AND created < %s',
[ilDBConstants::T_TEXT, ilDBConstants::T_INTEGER],
[$kind, $created]
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,62 +23,90 @@
* waiting for its authentication request, a Deep Linking request waiting for its response, a Dynamic
* Registration waiting for the tool. Each one is kept by its key for a while, and an expired one is gone.
*
* The requests belong to the session, but are not kept in it: ILIAS writes the whole session back when a
* request ends, so a request running in parallel in the same session would remove them again.
*
* @author Saúl Díaz <sdiaz@surlabs.com>
*/
final readonly class ilLTIAdvantagePendingRequests
{
private const string CREATED = 'created';

private ilLTIAdvantagePendingRequestStorage $storage;
private string $session;

/**
* @param int $lifetime seconds a request may wait
* @param string $kind what the requests are, which keeps them apart from the other ones of the session
* @param int $lifetime seconds a request may wait
* @param string|null $session_id the session the requests belong to, the current one by default
*/
public function __construct(private string $session_key, private int $lifetime)
{
public function __construct(
private string $kind,
private int $lifetime,
?ilLTIAdvantagePendingRequestStorage $storage = null,
?string $session_id = null
) {
global $DIC;

$this->storage = $storage ?? new ilLTIAdvantagePendingRequestDBStorage($DIC->database());
$session_id ??= session_id();
$this->session = $session_id !== false && $session_id !== '' ? hash('sha256', $session_id) : '';
}

/**
* @param array $request
*/
public function add(string $key, array $request): void
{
$requests = $this->getAll();
$requests[$key] = [self::CREATED => time()] + $request;
ilSession::set($this->session_key, $requests);
if ($this->session === '') {
return;
}
$now = time();
$this->storage->deleteOlderThan($this->kind, $now - $this->lifetime);
$this->storage->write($this->session, $this->kind, $this->hashKey($key), $request, $now);
}

/**
* @return array|null the request, null when it is not waiting
*/
public function get(string $key): ?array
{
return $this->getAll()[$key] ?? null;
if ($this->session === '') {
return null;
}
$kept = $this->storage->read($this->session, $this->kind, $this->hashKey($key));

return $this->isWaiting($kept) ? [self::CREATED => $kept[1]] + $kept[0] : null;
}

/**
* Ends the request, answered or not.
* Ends the request, answered or not. Only one of several requests taking it at the same time gets it.
*
* @return array|null the request, null when it was not waiting
*/
public function take(string $key): ?array
{
$requests = $this->getAll();
$request = $requests[$key] ?? null;
unset($requests[$key]);
ilSession::set($this->session_key, $requests);
if ($this->session === '') {
return null;
}
$kept = $this->storage->read($this->session, $this->kind, $this->hashKey($key));
if ($kept === null || !$this->storage->delete($this->session, $this->kind, $this->hashKey($key))) {
return null;
}

return $request;
return $this->isWaiting($kept) ? [self::CREATED => $kept[1]] + $kept[0] : null;
}

private function hashKey(string $key): string
{
return hash('sha256', $key);
}

/**
* @return array the requests still waiting, by key
* @param array{0: array, 1: int}|null $kept
*/
private function getAll(): array
private function isWaiting(?array $kept): bool
{
$requests = ilSession::get($this->session_key);

return array_filter(
is_array($requests) ? $requests : [],
fn($request): bool => is_array($request) && ($request[self::CREATED] ?? 0) >= time() - $this->lifetime
);
return $kept !== null && $kept[1] >= time() - $this->lifetime;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*
*********************************************************************/

declare(strict_types=1);

/**
* Where the requests of LTI Advantage a session waits to be answered are kept, by session, kind and key. Each
* request is a row of its own, so requests running in parallel in the same session never overwrite each other.
*
* @author Saúl Díaz <sdiaz@surlabs.com>
*/
interface ilLTIAdvantagePendingRequestStorage
{
/**
* @return array{0: array, 1: int}|null the request and when it was kept, null when there is none
*/
public function read(string $session, string $kind, string $key): ?array;

/**
* Keeps the request, replacing one with the same session, kind and key.
*
* @param array $request
*/
public function write(string $session, string $kind, string $key, array $request, int $created): void;

/**
* @return bool whether there was a request to remove
*/
public function delete(string $session, string $kind, string $key): bool;

/**
* Removes the requests of the kind kept before the given time, of all sessions.
*/
public function deleteOlderThan(string $kind, int $created): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* LTI Advantage Deep Linking with ILIAS as platform: a user who creates LTI objects in a container picks the
* content in the tool, and each resource link the tool sends back becomes an object.
*
* A request waits in the session of the user, for that user and container, and is used once: its state goes
* A request waits for the session of the user, for that user and container, and is used once: its state goes
* to the tool as the data of the request and has to come back with the response. The session cookie is sent
* as SameSite=None when the request starts, because the tool posts the response from its own site.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
* ltiregistration.php, with the token, from its server: that request has no session of ILIAS, so the token
* carries who registers and the client id the tool gets. The tool is stored as an own tool of that user, and
* the token is used up with it. When the tool says it is done, the page of the user takes the tool the
* registration left in the session and creates the object for it.
* registration left for the session and creates the object for it.
*
* @author Saúl Díaz <sdiaz@surlabs.com>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
* the library, with its key and URLs. The library does the protocol: the OpenID Connect login, the
* id_token, the check of the client assertion of a token request and the access token.
*
* The login a launch starts is kept in the session of the user until the tool asks for the id_token at
* The login a launch starts is kept for the session of the user until the tool asks for the id_token at
* ltiauth.php. Unlike the library, an authentication request without such a login is refused.
*
* An embedded launch offers the tool the platform storage of LTI (postMessage to the page around the
Expand Down Expand Up @@ -166,19 +166,21 @@ public function verifyClientAssertion(string $assertion): bool

/**
* The page that starts the launch of an object: it sends the browser to the login initiation URL of
* the tool. The launch itself waits in the session for the authentication request of the tool.
* the tool. The launch itself waits for the authentication request of the tool, under a hint of its own,
* so launches of the same object running at the same time, in two tabs for one, do not replace each other.
*
* @param array $parameters see ilLTIAdvantagePlatformLaunchParameterBuilder
* @param bool $embedded true when the tool opens in an iframe of the page that handles the platform storage
* @param string $url the target link of the object, when it has its own instead of the one of the tool
* @throws RandomException
*/
public function getLaunchPage(int $ref_id, array $parameters, bool $embedded, string $url = ''): string
{
return $this->getMessagePage(
$url !== '' ? $url : Tool::$defaultTool->messageUrl,
self::MESSAGE_LAUNCH,
$parameters,
(string) $ref_id,
$ref_id . '.' . bin2hex(random_bytes(8)),
$embedded
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,4 +517,23 @@ public function step_34(): void
]);
}
}

/**
* The requests of LTI Advantage a session waits to be answered: logins, Deep Linking requests and Dynamic
* Registrations. They are not kept in the session, which a parallel request of it writes back later.
*/
public function step_35(): void
{
if (!$this->db->tableExists('lti_pending_requests')) {
$this->db->createTable('lti_pending_requests', [
'session_id' => ['type' => 'text', 'length' => 64, 'notnull' => true],
'kind' => ['type' => 'text', 'length' => 64, 'notnull' => true],
'request_key' => ['type' => 'text', 'length' => 64, 'notnull' => true],
'request_data' => ['type' => 'clob', 'notnull' => true],
'created' => ['type' => 'integer', 'length' => 4, 'notnull' => true, 'default' => 0]
]);
$this->db->addPrimaryKey('lti_pending_requests', ['session_id', 'kind', 'request_key']);
$this->db->addIndex('lti_pending_requests', ['kind', 'created'], 'i1');
}
}
}
Loading
Loading