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
1 change: 1 addition & 0 deletions components/ILIAS/ResourceStorage/PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Disclaimer: This documentation does not warrant completeness or correctness. Ple
## Data being presented
- Persons with "Read" permission for the FileServices object are presented with the date and time of the resource creation in the Resource Overview.
- The Resource Overview is located under "Administration" > "System Settings and Maintenance" > "File Services" > "Resource Overview".
- For resources marked as confidential, file names and revision titles are only presented to the owner of the current revision. Persons with "Read" permission for the FileServices object can neither see them nor download the resource in the Resource Overview.

## Data being deleted
- Personal data that is stored regarding resources or revisions is deleted by the IRSS when directed to do so by the component who created the resource or revision.
Expand Down
36 changes: 36 additions & 0 deletions components/ILIAS/ResourceStorage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,42 @@ $latest_published_revision = $irss->manage()->getCurrentRevision($rid); // First
$latest_draft_revision = $irss->manage()->getCurrentRevisionIncludingDraft($rid); // Second Upload
```

# Confidential Resources

All resources are listed in the Resource Overview of the administration ("Administration" > "System Settings and
Maintenance" > "File Services" > "Resource Overview"). Resources containing sensitive files (e.g. private uploads of a
user) can be marked as confidential. For confidential resources the Resource Overview
- shows `[Confidential]` instead of the file name and the revision titles,
- does not offer a download (neither as action nor via a direct link),
- does not find them via the title filter,

unless the person looking at the overview is the owner of the current revision of the resource. Size, file type,
creation date and stakeholders are still displayed.

Confidentiality only affects the Resource Overview. Delivering resources via `consume()` is still controlled by the
consuming component and its stakeholder.

A stakeholder can mark all resources created for it as confidential by implementing `ConfidentialStakeholder`:

```php
class ilMyComponentResourceStakeholder extends AbstractResourceStakeholder implements ConfidentialStakeholder
{
public function areNewResourcesConfidential(): bool
{
return true;
}
}
```

The flag can also be set or removed for an existing resource:

```php
$irss->manage()->setConfidential($rid, true);
$irss->manage()->isConfidential($rid); // true
```

Cloned resources keep the flag of the original resource.

# Collections

In many cases a component does not only need a single resource to be stored, but wants to be able to use a collection of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,13 @@ public function get(ResourceIdentification $identification): StorableResource
return $this->cache[$rid];
}

$q = "SELECT storage_id, rtype FROM " . self::TABLE_NAME . " WHERE " . self::IDENTIFICATION . " = %s";
$q = "SELECT storage_id, rtype, confidential FROM " . self::TABLE_NAME . " WHERE " . self::IDENTIFICATION . " = %s";
$r = $this->db->queryF($q, ['text'], [$rid]);
$d = $this->db->fetchObject($r);

$resource = $this->blank($identification, ResourceType::from($d->rtype));
$resource = $this->blank($identification, ResourceType::from((int) $d->rtype));
$resource->setStorageID($d->storage_id);
$resource->setConfidential((bool) $d->confidential);

$this->cache[$rid] = $resource;

Expand Down Expand Up @@ -119,7 +120,8 @@ public function store(StorableResource $resource): void
],
[
'storage_id' => ['text', $resource->getStorageID()],
'rtype' => ['text', $resource->getType()->value],
'rtype' => ['integer', $resource->getType()->value],
'confidential' => ['integer', (int) $resource->isConfidential()],
]
);

Expand Down Expand Up @@ -151,7 +153,7 @@ public function getAll(): \Generator
public function preload(array $identification_strings): void
{
$r = $this->db->query(
"SELECT rid, storage_id, rtype FROM " . self::TABLE_NAME . " WHERE "
"SELECT rid, storage_id, rtype, confidential FROM " . self::TABLE_NAME . " WHERE "
. $this->db->in(self::IDENTIFICATION, $identification_strings, false, 'text')
);
while ($d = $this->db->fetchAssoc($r)) {
Expand All @@ -163,9 +165,10 @@ public function populateFromArray(array $data): void
{
$resource = $this->blank(
new ResourceIdentification($data['rid']),
ResourceType::from($data['rtype'] ?? ResourceType::SINGLE_FILE->value)
ResourceType::from((int) ($data['rtype'] ?? ResourceType::SINGLE_FILE->value))
);
$resource->setStorageID($data['storage_id']);
$resource->setConfidential((bool) ($data['confidential'] ?? false));
$this->cache[$data['rid']] = $resource;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,13 @@
*/
class AllResourcesDataSource extends BaseTableDataSource implements TableDataSource
{
public function __construct()
{
/**
* @param int|null $acting_user_id Owner whose confidential resources may be found by title, see
* {@see \ILIAS\components\ResourceStorage\Resources\UI\ConfidentialityPolicy}
*/
public function __construct(
private readonly ?int $acting_user_id = null
) {
global $DIC;
parent::__construct(
$DIC->resourceStorage()->collection()->id() // ad hoc collection
Expand Down Expand Up @@ -135,7 +140,8 @@ private function getBaseQuery(bool $count_only): string
$q .= " FROM il_resource_revision
JOIN il_resource_info ON il_resource_revision.rid = il_resource_info.rid AND
il_resource_info.version_number = il_resource_revision.version_number
JOIN il_resource_stkh_u ON il_resource_revision.rid = il_resource_stkh_u.rid";
JOIN il_resource_stkh_u ON il_resource_revision.rid = il_resource_stkh_u.rid
JOIN il_resource ON il_resource_revision.rid = il_resource.rid";

// WHERE
$q .= $this->buildQueryFilter();
Expand Down Expand Up @@ -184,6 +190,9 @@ private function buildQueryFilter(): string
if (isset($this->filter_values['title']) && $this->filter_values['title'] !== '') {
$filter_values[] = $this->db->quote("%" . $this->filter_values['title'] . "%", 'text');
$filters[] = " il_resource_revision.title LIKE %s ";
// titles of confidential resources must not be searchable, except for their owner
$filter_values[] = $this->db->quote($this->acting_user_id ?? 0, 'integer');
$filters[] = " (il_resource.confidential = 0 OR il_resource_revision.owner_id = %s) ";
}
if (isset($this->filter_values['size']) && $this->filter_values['size'] !== '') {
$greater_than = (int) $this->filter_values['size'] * BaseToComponent::SIZE_FACTOR * BaseToComponent::SIZE_FACTOR;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use ILIAS\ResourceStorage\Services;
use ILIAS\ResourceStorage\Revision\Revision;
use ILIAS\UI\Component\Modal\Modal;
use ILIAS\components\ResourceStorage\Resources\UI\ConfidentialityPolicy;

/**
* @author Fabian Schmid <fabian@sr.solutions>
Expand All @@ -38,8 +39,9 @@ class OverviewActionGenerator implements ActionGenerator
private array $collected_modals = [];
private Services $irss;

public function __construct()
{
public function __construct(
private ConfidentialityPolicy $confidentiality_policy
) {
global $DIC;
$this->ui_factory = $DIC->ui()->factory();
$this->language = $DIC->language();
Expand All @@ -55,6 +57,8 @@ public function getActionsForRevision(
\ilResourceOverviewGUI::P_RESOURCE_ID,
$revision->getIdentification()->serialize()
);
$resource = $this->irss->manage()->getResource($revision->getIdentification());
$redacted = $this->confidentiality_policy->isRedacted($resource);
$actions = [
$this->ui_factory->button()->shy(
$this->language->txt('action_show_revisions'),
Expand All @@ -70,17 +74,18 @@ public function getActionsForRevision(
\ilResourceOverviewGUI::CMD_GOTO_RESOURCE
)
),
$this->ui_factory->button()->shy(
];
if (!$redacted) {
$actions[] = $this->ui_factory->button()->shy(
$this->language->txt('action_download'),
$this->ctrl->getLinkTargetByClass(
\ilResourceOverviewGUI::class,
\ilResourceOverviewGUI::CMD_DOWNLOAD
)
),
];
$resource = $this->irss->manage()->getResource($revision->getIdentification());
);
}
if ($resource->getStakeholders() === []) {
$this->collected_modals[] = $modal = $this->getRemoveConfirmationModal($revision);
$this->collected_modals[] = $modal = $this->getRemoveConfirmationModal($revision, $redacted);
$actions[] = $this->ui_factory->button()->shy(
$this->language->txt('action_remove_resource'),
'#'
Expand All @@ -97,7 +102,7 @@ public function getCollectedModals(): array
}


private function getRemoveConfirmationModal(Revision $revision): Modal
private function getRemoveConfirmationModal(Revision $revision, bool $redacted): Modal
{
$action = $this->ctrl->getLinkTargetByClass(
\ilResourceOverviewGUI::class,
Expand All @@ -110,7 +115,9 @@ private function getRemoveConfirmationModal(Revision $revision): Modal
)->withAffectedItems([
$this->ui_factory->modal()->interruptiveItem()->standard(
$revision->getIdentification()->serialize(),
$revision->getInformation()->getTitle(),
$redacted
? $this->language->txt('confidential_redacted')
: $revision->getInformation()->getTitle(),
)
]);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?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);

namespace ILIAS\components\ResourceStorage\Resources\UI;

use ILIAS\ResourceStorage\Resource\StorableResource;

/**
* Decides whether a confidential resource has to be redacted for the acting user:
* only the owner of the current revision gets to see file names and download it.
*
* @author Fabian Schmid <fabian@sr.solutions>
*/
class ConfidentialityPolicy
{
public function __construct(
private readonly int $acting_user_id
) {
}

public function isRedacted(StorableResource $resource): bool
{
if (!$resource->isConfidential()) {
return false;
}

return $resource->getCurrentRevisionIncludingDraft()->getOwnerId() !== $this->acting_user_id;
}

public function getActingUserId(): int
{
return $this->acting_user_id;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class ResourceListingUI
public function __construct(
private ViewDefinition $view_definition,
private TableDataSource $data_source,
private ActionGenerator $action_generator = new NullActionGenerator()
private ActionGenerator $action_generator = new NullActionGenerator(),
private ?ConfidentialityPolicy $confidentiality_policy = null
) {
global $DIC;
$this->ctrl = $DIC->ctrl();
Expand Down Expand Up @@ -125,7 +126,8 @@ public function getRowMapping(): \Closure
$resource = $this->irss->manage()->getResource($resource_identification);
$resource_to_component = new ResourceToComponent(
$resource,
$this->action_generator
$this->action_generator,
$this->confidentiality_policy
);
return $resource_to_component->getAsRowMapping()($row, $resource_identification);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,16 @@ class ResourceToComponent extends BaseToComponent implements ToComponent

public function __construct(
protected StorableResource $resource,
?ActionGenerator $action_generator = null
?ActionGenerator $action_generator = null,
?ConfidentialityPolicy $confidentiality_policy = null
) {
parent::__construct($action_generator);
$this->current_revision = $this->resource->getCurrentRevision();
$this->revision_to_component = new RevisionToComponent(
$this->current_revision,
$action_generator
$action_generator,
$this->resource->isConfidential(),
$confidentiality_policy?->isRedacted($this->resource) ?? $this->resource->isConfidential()
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class RevisionListingUI
public function __construct(
private ViewDefinition $view_definition,
protected StorableResource $resource,
private ?ConfidentialityPolicy $confidentiality_policy = null,
) {
global $DIC;
$this->language = $DIC->language();
Expand All @@ -56,9 +57,7 @@ public function __construct(
private function initItems(): void
{
$this->components = array_map(function (Revision $revision): Item {
$revision_to_component = new RevisionToComponent(
$revision
);
$revision_to_component = $this->getRevisionToComponent($revision);
$item = $revision_to_component->getAsItem(false);
return $item->withLeadText($this->language->txt('revision') . ' ' . $revision->getVersionNumber());
}, array_reverse($this->resource->getAllRevisionsIncludingDraft()));
Expand All @@ -69,9 +68,7 @@ private function initDeck(): void
$this->components[] = $this->ui_factory->deck(
array_map(
function (Revision $revision): Card {
$revision_to_component = new RevisionToComponent(
$revision
);
$revision_to_component = $this->getRevisionToComponent($revision);
$card = $revision_to_component->getAsCard();
return $card->withTitle($this->prependRevisionNumberToTitle($revision, $card->getTitle()));
},
Expand All @@ -98,9 +95,7 @@ public function getRowMapping(): \Closure
PresentationRow $row,
Revision $revision
): PresentationRow {
$revision_to_component = new RevisionToComponent(
$revision
);
$revision_to_component = $this->getRevisionToComponent($revision);
$row = $revision_to_component->getAsRowMapping()($row, $revision->getIdentification());
/** @var PresentationRow $row */
$title = $row->getHeadline();
Expand All @@ -111,6 +106,16 @@ public function getRowMapping(): \Closure
}


private function getRevisionToComponent(Revision $revision): RevisionToComponent
{
return new RevisionToComponent(
$revision,
null,
$this->resource->isConfidential(),
$this->confidentiality_policy?->isRedacted($this->resource) ?? $this->resource->isConfidential()
);
}

public function getComponents(): array
{
return $this->components;
Expand Down
Loading
Loading