Skip to content

Commit

Permalink
MDL-64322 GDPR: Mechanism for restricting delete requests
Browse files Browse the repository at this point in the history
  • Loading branch information
Kiet.Chan committed Apr 8, 2019
1 parent 9d4f4f0 commit 84bcd65
Show file tree
Hide file tree
Showing 12 changed files with 375 additions and 10 deletions.
54 changes: 54 additions & 0 deletions admin/tool/dataprivacy/classes/api.php
Expand Up @@ -614,6 +614,12 @@ public static function approve_data_request($requestid) {
throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
}

// Check if current user has permission to approve delete data request.
if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) {
throw new required_capability_exception(context_system::instance(),
'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
}

// Update the status and the DPO.
$result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id);

Expand Down Expand Up @@ -653,6 +659,12 @@ public static function deny_data_request($requestid) {
throw new moodle_exception('errorrequestnotwaitingforapproval', 'tool_dataprivacy');
}

// Check if current user has permission to reject delete data request.
if ($request->get('type') == self::DATAREQUEST_TYPE_DELETE && !self::can_create_data_deletion_request_for_other()) {
throw new required_capability_exception(context_system::instance(),
'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
}

// Update the status and the DPO.
return self::update_request_status($requestid, self::DATAREQUEST_STATUS_REJECTED, $USER->id);
}
Expand Down Expand Up @@ -752,6 +764,48 @@ public static function require_can_create_data_request_for_user($user, $requeste
return true;
}

/**
* Check if user has permisson to create data deletion request for themselves.
*
* @param int|null $userid ID of the user.
* @return bool
* @throws coding_exception
*/
public static function can_create_data_deletion_request_for_self(int $userid = null): bool {
global $USER;
$userid = $userid ?: $USER->id;
return has_capability('tool/dataprivacy:requestdelete', \context_user::instance($userid), $userid);
}

/**
* Check if user has permission to create data deletion request for another user.
*
* @param int|null $userid ID of the user.
* @return bool
* @throws coding_exception
* @throws dml_exception
*/
public static function can_create_data_deletion_request_for_other(int $userid = null): bool {
global $USER;
$userid = $userid ?: $USER->id;
return has_capability('tool/dataprivacy:requestdeleteforotheruser', context_system::instance(), $userid);
}

/**
* Check if parent can create data deletion request for their children.
*
* @param int $userid ID of a user being requested.
* @param int|null $requesterid ID of a user making request.
* @return bool
* @throws coding_exception
*/
public static function can_create_data_deletion_request_for_children(int $userid, int $requesterid = null): bool {
global $USER;
$requesterid = $requesterid ?: $USER->id;
return has_capability('tool/dataprivacy:makedatadeletionrequestsforchildren', \context_user::instance($userid),
$requesterid);
}

/**
* Checks whether a user can download a data request.
*
Expand Down
Expand Up @@ -170,6 +170,10 @@ protected function get_other_values(renderer_base $output) {
$values['canreview'] = true;
// Whether the DPO can approve or deny the request.
$values['approvedeny'] = in_array($requesttype, [api::DATAREQUEST_TYPE_EXPORT, api::DATAREQUEST_TYPE_DELETE]);
// If the request's type is delete, check if user have permission to approve/deny it.
if ($requesttype == api::DATAREQUEST_TYPE_DELETE) {
$values['approvedeny'] = api::can_create_data_deletion_request_for_other();
}
break;
case api::DATAREQUEST_STATUS_APPROVED:
$values['statuslabelclass'] = 'badge-info';
Expand Down
18 changes: 17 additions & 1 deletion admin/tool/dataprivacy/classes/output/data_requests_table.php
Expand Up @@ -116,9 +116,17 @@ public function __construct($userid = 0, $statuses = [], $types = [], $creationm
*
* @param stdClass $data The row data.
* @return string
* @throws \moodle_exception
* @throws coding_exception
*/
public function col_select($data) {
if ($data->status == \tool_dataprivacy\api::DATAREQUEST_STATUS_AWAITING_APPROVAL) {
if ($data->type == \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE
&& !api::can_create_data_deletion_request_for_other()) {
// Don't show checkbox if request's type is delete and user don't have permission.
return false;
}

$stringdata = [
'username' => $data->foruser->fullname,
'requesttype' => \core_text::strtolower($data->typenameshort)
Expand Down Expand Up @@ -206,6 +214,7 @@ public function col_actions($data) {

$requestid = $data->id;
$status = $data->status;
$persistent = $this->datarequests[$requestid];

// Prepare actions.
$actions = [];
Expand All @@ -232,6 +241,11 @@ public function col_actions($data) {
}
break;
case api::DATAREQUEST_STATUS_AWAITING_APPROVAL:
// Only show "Approve" and "Deny" button for deletion request if current user has permission.
if ($persistent->get('type') == api::DATAREQUEST_TYPE_DELETE &&
!api::can_create_data_deletion_request_for_other()) {
break;
}
// Approve.
$actiondata['data-action'] = 'approve';
$actiontext = get_string('approverequest', 'tool_dataprivacy');
Expand All @@ -253,9 +267,11 @@ public function col_actions($data) {
}

if ($this->manage) {
$persistent = $this->datarequests[$requestid];
$canreset = $persistent->is_active() || empty($this->ongoingrequests[$data->foruser->id]->{$data->type});
$canreset = $canreset && $persistent->is_resettable();
// Prevent re-submmit deletion request if current user don't have permission.
$canreset = $canreset && ($persistent->get('type') != api::DATAREQUEST_TYPE_DELETE ||
api::can_create_data_deletion_request_for_other());
if ($canreset) {
$reseturl = new moodle_url('/admin/tool/dataprivacy/resubmitrequest.php', [
'requestid' => $requestid,
Expand Down
15 changes: 14 additions & 1 deletion admin/tool/dataprivacy/createdatarequest.php
Expand Up @@ -76,6 +76,19 @@
}
}

if ($data->type == \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE) {
if ($data->userid == $USER->id) {
if (!\tool_dataprivacy\api::can_create_data_deletion_request_for_self()) {
throw new moodle_exception('nopermissions', 'error', '',
get_string('errorcannotrequestdeleteforself', 'tool_dataprivacy'));
}
} else if (!\tool_dataprivacy\api::can_create_data_deletion_request_for_other()
&& !\tool_dataprivacy\api::can_create_data_deletion_request_for_children($data->userid)) {
throw new moodle_exception('nopermissions', 'error', '',
get_string('errorcannotrequestdeleteforother', 'tool_dataprivacy'));
}
}

\tool_dataprivacy\api::create_data_request($data->userid, $data->type, $data->comments);

if ($manage) {
Expand All @@ -93,7 +106,7 @@
echo $OUTPUT->header();
echo $OUTPUT->heading($title);

echo $OUTPUT->box_start();
echo $OUTPUT->box_start('createrequestform');
$mform->display();
echo $OUTPUT->box_end();

Expand Down
33 changes: 33 additions & 0 deletions admin/tool/dataprivacy/createdatarequest_form.php
Expand Up @@ -45,6 +45,7 @@ class tool_dataprivacy_data_request_form extends moodleform {
* Form definition.
*
* @throws coding_exception
* @throws dml_exception
*/
public function definition() {
global $USER;
Expand Down Expand Up @@ -108,6 +109,24 @@ public function definition() {
// Action buttons.
$this->add_action_buttons();

$shouldfreeze = false;
if ($this->manage) {
$shouldfreeze = !api::can_create_data_deletion_request_for_other();
} else {
$shouldfreeze = !api::can_create_data_deletion_request_for_self();
if ($shouldfreeze && !empty($useroptions)) {
foreach ($useroptions as $userid => $useroption) {
if (api::can_create_data_deletion_request_for_children($userid)) {
$shouldfreeze = false;
break;
}
}
}
}

if ($shouldfreeze) {
$mform->freeze('type');
}
}

/**
Expand All @@ -120,6 +139,7 @@ public function definition() {
* @throws dml_exception
*/
public function validation($data, $files) {
global $USER;
$errors = [];

$validrequesttypes = [
Expand All @@ -134,6 +154,19 @@ public function validation($data, $files) {
$errors['type'] = get_string('errorrequestalreadyexists', 'tool_dataprivacy');
}

// Check if current user can create data deletion request.
$userid = $data['userid'];
if ($data['type'] == api::DATAREQUEST_TYPE_DELETE) {
if ($userid == $USER->id) {
if (!api::can_create_data_deletion_request_for_self()) {
$errors['type'] = get_string('errorcannotrequestdeleteforself', 'tool_dataprivacy');
}
} else if (!api::can_create_data_deletion_request_for_other()
&& !api::can_create_data_deletion_request_for_children($userid)) {
$errors['type'] = get_string('errorcannotrequestdeleteforother', 'tool_dataprivacy');
}
}

return $errors;
}
}
28 changes: 28 additions & 0 deletions admin/tool/dataprivacy/db/access.php
Expand Up @@ -34,6 +34,15 @@
'archetypes' => []
],

// Capability for create new delete data request. Usually given to the site's Protection Officer.
'tool/dataprivacy:requestdeleteforotheruser' => [
'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_DATALOSS,
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [],
'clonepermissionsfrom' => 'tool/dataprivacy:managedatarequests'
],

// Capability for managing the data registry. Usually given to the site's Data Protection Officer.
'tool/dataprivacy:managedataregistry' => [
'riskbitmask' => RISK_SPAM | RISK_PERSONAL | RISK_XSS | RISK_DATALOSS,
Expand All @@ -50,6 +59,15 @@
'archetypes' => []
],

// Capability for parents/guardians to make delete data requests on behalf of their children.
'tool/dataprivacy:makedatadeletionrequestsforchildren' => [
'riskbitmask' => RISK_SPAM | RISK_PERSONAL,
'captype' => 'write',
'contextlevel' => CONTEXT_USER,
'archetypes' => [],
'clonepermissionsfrom' => 'tool/dataprivacy:makedatarequestsforchildren'
],

// Capability for users to download the results of their own data request.
'tool/dataprivacy:downloadownrequest' => [
'riskbitmask' => 0,
Expand All @@ -67,4 +85,14 @@
'contextlevel' => CONTEXT_USER,
'archetypes' => []
],

// Capability for users to create delete data request for their own.
'tool/dataprivacy:requestdelete' => [
'riskbitmask' => RISK_DATALOSS,
'captype' => 'write',
'contextlevel' => CONTEXT_USER,
'archetypes' => [
'user' => CAP_ALLOW
]
]
];
6 changes: 6 additions & 0 deletions admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
Expand Up @@ -70,6 +70,7 @@
$string['contextpurposecategorysaved'] = 'Purpose and category saved.';
$string['contactdpoviaprivacypolicy'] = 'Please contact the privacy officer as described in the privacy policy.';
$string['createcategory'] = 'Create data category';
$string['createdeletedatarequest'] = 'Create data deletion request';
$string['createnewdatarequest'] = 'Create a new data request';
$string['createpurpose'] = 'Create data purpose';
$string['creationauto'] = 'Automatically';
Expand All @@ -81,6 +82,9 @@
$string['dataprivacy:managedataregistry'] = 'Manage data registry';
$string['dataprivacy:downloadownrequest'] = 'Download your own exported data';
$string['dataprivacy:downloadallrequests'] = 'Download exported data for everyone';
$string['dataprivacy:requestdeleteforotheruser'] = 'Request data deletion on behalf of another user';
$string['dataprivacy:makedatadeletionrequestsforchildren'] = 'Request data deletion for minors';
$string['dataprivacy:requestdelete'] = 'Request data deletion for yourself';
$string['dataregistry'] = 'Data registry';
$string['dataregistryinfo'] = 'The data registry enables categories (types of data) and purposes (the reasons for processing data) to be set for all content on the site - from users and courses down to activities and blocks. For each purpose, a retention period may be set. When a retention period has expired, the data is flagged and listed for deletion, awaiting admin confirmation.';
$string['dataretentionexplanation'] = 'This summary shows the default categories and purposes for retaining user data. Certain areas may have more specific categories and purposes than those listed here.';
Expand Down Expand Up @@ -124,6 +128,8 @@
$string['effectiveretentionperiodcourse'] = '{$a} (after the course end date)';
$string['effectiveretentionperioduser'] = '{$a} (since the last time the user accessed the site)';
$string['emailsalutation'] = 'Dear {$a},';
$string['errorcannotrequestdeleteforself'] = 'You don\'t have permission to create deletion request for yourself.';
$string['errorcannotrequestdeleteforother'] = 'You don\'t have permission to create deletion request for this user.';
$string['errorinvalidrequestcreationmethod'] = 'Invalid request creation method!';
$string['errorinvalidrequeststatus'] = 'Invalid request status!';
$string['errorinvalidrequesttype'] = 'Invalid request type!';
Expand Down
5 changes: 3 additions & 2 deletions admin/tool/dataprivacy/lib.php
Expand Up @@ -77,8 +77,9 @@ function tool_dataprivacy_myprofile_navigation(tree $tree, $user, $iscurrentuser

// Check if the user has an ongoing data deletion request.
$hasdeleterequest = \tool_dataprivacy\api::has_ongoing_request($user->id, \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE);
// Show data deletion link only if the user doesn't have an ongoing data deletion request.
if (!$hasdeleterequest) {
// Show data deletion link only if the user doesn't have an ongoing data deletion request and has permission
// to create data deletion request.
if (!$hasdeleterequest && \tool_dataprivacy\api::can_create_data_deletion_request_for_self()) {
$deleteparams = ['type' => \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE];
$deleteurl = new moodle_url('/admin/tool/dataprivacy/createdatarequest.php', $deleteparams);
$deletenode = new core_user\output\myprofile\node('privacyandpolicies', 'requestdatadeletion',
Expand Down
5 changes: 5 additions & 0 deletions admin/tool/dataprivacy/resubmitrequest.php
Expand Up @@ -44,6 +44,11 @@
];

if (null !== $confirm && confirm_sesskey()) {
if ($originalrequest->get('type') == \tool_dataprivacy\api::DATAREQUEST_TYPE_DELETE
&& !\tool_dataprivacy\api::can_create_data_deletion_request_for_other()) {
throw new required_capability_exception(context_system::instance(),
'tool/dataprivacy:requestdeleteforotheruser', 'nopermissions', '');
}
$originalrequest->resubmit_request();
redirect($manageurl, get_string('resubmittedrequest', 'tool_dataprivacy', $stringparams));
}
Expand Down

0 comments on commit 84bcd65

Please sign in to comment.