Skip to content

Commit

Permalink
MDL-61652 tool_dataprivacy: Add capabilities to control data downloads
Browse files Browse the repository at this point in the history
  • Loading branch information
sammarshallou authored and Mihail Geshoski committed Jul 24, 2018
1 parent 0180369 commit 635c7b2
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 31 deletions.
48 changes: 48 additions & 0 deletions admin/tool/dataprivacy/classes/api.php
Expand Up @@ -608,6 +608,54 @@ public static function can_create_data_request_for_user($user, $requester = null
return has_capability('tool/dataprivacy:makedatarequestsforchildren', $usercontext, $requester);
}

/**
* Checks whether a user can download a data request.
*
* @param int $userid Target user id (subject of data request)
* @param int $requesterid Requester user id (person who requsted it)
* @param int|null $downloaderid Person who wants to download user id (default current)
* @return bool
* @throws coding_exception
*/
public static function can_download_data_request_for_user($userid, $requesterid, $downloaderid = null) {
global $USER;

if (!$downloaderid) {
$downloaderid = $USER->id;
}

$usercontext = \context_user::instance($userid);
// If it's your own and you have the right capability, you can download it.
if ($userid == $downloaderid && has_capability('tool/dataprivacy:downloadownrequest', $usercontext, $downloaderid)) {
return true;
}
// If you can download anyone's in that context, you can download it.
if (has_capability('tool/dataprivacy:downloadallrequests', $usercontext, $downloaderid)) {
return true;
}
// If you can have the 'child access' ability to request in that context, and you are the one
// who requested it, then you can download it.
if ($requesterid == $downloaderid && self::can_create_data_request_for_user($userid, $requesterid)) {
return true;
}
return false;
}

/**
* Gets an action menu link to download a data request.
*
* @param \context_user $usercontext User context (of user who the data is for)
* @param int $requestid Request id
* @return \action_menu_link_secondary Action menu link
* @throws coding_exception
*/
public static function get_download_link(\context_user $usercontext, $requestid) {
$downloadurl = moodle_url::make_pluginfile_url($usercontext->id,
'tool_dataprivacy', 'export', $requestid, '/', 'export.zip', true);
$downloadtext = get_string('download', 'tool_dataprivacy');
return new \action_menu_link_secondary($downloadurl, null, $downloadtext);
}

/**
* Creates a new data purpose.
*
Expand Down
8 changes: 8 additions & 0 deletions admin/tool/dataprivacy/classes/output/data_requests_table.php
Expand Up @@ -208,6 +208,14 @@ public function col_actions($data) {
break;
}

if ($status == api::DATAREQUEST_STATUS_COMPLETE) {
$userid = $data->foruser->id;
$usercontext = \context_user::instance($userid, IGNORE_MISSING);
if ($usercontext && api::can_download_data_request_for_user($userid, $data->requestedbyuser->id)) {
$actions[] = api::get_download_link($usercontext, $requestid);
}
}

$actionsmenu = new action_menu($actions);
$actionsmenu->set_menu_trigger(get_string('actions'));
$actionsmenu->set_owner_selector('request-actions-' . $requestid);
Expand Down
12 changes: 7 additions & 5 deletions admin/tool/dataprivacy/classes/output/my_data_requests_page.php
Expand Up @@ -95,7 +95,8 @@ public function export_for_template(renderer_base $output) {
$requestexporter = new data_request_exporter($request, ['context' => $outputcontext]);
$item = $requestexporter->export($output);

if ($request->get('userid') != $USER->id) {
$self = $request->get('userid') == $USER->id;
if (!$self) {
// Append user name if it differs from $USER.
$a = (object)['typename' => $item->typename, 'user' => $item->foruser->fullname];
$item->typename = get_string('requesttypeuser', 'tool_dataprivacy', $a);
Expand All @@ -110,6 +111,10 @@ public function export_for_template(renderer_base $output) {
$cancancel = false;
// Show download links only for export-type data requests.
$candownload = $type == api::DATAREQUEST_TYPE_EXPORT;
if ($usercontext) {
$candownload = api::can_download_data_request_for_user(
$request->get('userid'), $request->get('requestedby'));
}
break;
case api::DATAREQUEST_STATUS_CANCELLED:
case api::DATAREQUEST_STATUS_REJECTED:
Expand All @@ -126,10 +131,7 @@ public function export_for_template(renderer_base $output) {
$actions[] = new action_menu_link_secondary($cancelurl, null, $canceltext, $canceldata);
}
if ($candownload && $usercontext) {
$downloadurl = moodle_url::make_pluginfile_url($usercontext->id, 'tool_dataprivacy', 'export', $requestid, '/',
'export.zip', true);
$downloadtext = get_string('download', 'tool_dataprivacy');
$actions[] = new action_menu_link_secondary($downloadurl, null, $downloadtext);
$actions[] = api::get_download_link($usercontext, $requestid);
}
if (!empty($actions)) {
$actionsmenu = new action_menu($actions);
Expand Down
49 changes: 40 additions & 9 deletions admin/tool/dataprivacy/classes/task/process_data_request_task.php
Expand Up @@ -139,8 +139,17 @@ public function execute() {

$output = $PAGE->get_renderer('tool_dataprivacy');
$emailonly = false;
$notifyuser = true;
switch ($request->type) {
case api::DATAREQUEST_TYPE_EXPORT:
// Check if the user is allowed to download their own export. (This is for
// institutions which centrally co-ordinate subject access request across many
// systems, not just one Moodle instance, so we don't want every instance emailing
// the user.)
if (!api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->userid)) {
$notifyuser = false;
}

$typetext = get_string('requesttypeexport', 'tool_dataprivacy');
// We want to notify the user in Moodle about the processing results.
$message->notification = 1;
Expand Down Expand Up @@ -179,18 +188,40 @@ public function execute() {
$message->fullmessagehtml = $messagehtml;

// Send message to the user involved.
if ($emailonly) {
email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
} else {
message_send($message);
if ($notifyuser) {
if ($emailonly) {
email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml);
} else {
message_send($message);
}
mtrace('Message sent to user: ' . $messagetextdata['username']);
}
mtrace('Message sent to user: ' . $messagetextdata['username']);

// Send to requester as well if this request was made on behalf of another user who's not a DPO,
// and has the capability to make data requests for the user (e.g. Parent).
if (!api::is_site_dpo($request->requestedby) && $foruser->id != $request->requestedby) {
// Send to requester as well in some circumstances.
if ($foruser->id != $request->requestedby) {
$sendtorequester = false;
switch ($request->type) {
case api::DATAREQUEST_TYPE_EXPORT:
// Send to the requester as well if they can download it, unless they are the
// DPO. If we didn't notify the user themselves (because they can't download)
// then send to requester even if it is the DPO, as in that case the requester
// needs to take some action.
if (api::can_download_data_request_for_user($request->userid, $request->requestedby, $request->requestedby)) {
$sendtorequester = !$notifyuser || !api::is_site_dpo($request->requestedby);
}
break;
case api::DATAREQUEST_TYPE_DELETE:
// Send to the requester if they are not the DPO and if they are allowed to
// create data requests for the user (e.g. Parent).
$sendtorequester = !api::is_site_dpo($request->requestedby) &&
api::can_create_data_request_for_user($request->userid, $request->requestedby);
break;
default:
throw new moodle_exception('errorinvalidrequesttype', 'tool_dataprivacy');
}

// Ensure the requester has the capability to make data requests for this user.
if (api::can_create_data_request_for_user($request->userid, $request->requestedby)) {
if ($sendtorequester) {
$requestedby = core_user::get_user($request->requestedby);
$message->userto = $requestedby;
$messagetextdata['username'] = fullname($requestedby);
Expand Down
18 changes: 18 additions & 0 deletions admin/tool/dataprivacy/db/access.php
Expand Up @@ -49,4 +49,22 @@
'contextlevel' => CONTEXT_USER,
'archetypes' => []
],

// Capability for users to download the results of their own data request.
'tool/dataprivacy:downloadownrequest' => [
'riskbitmask' => 0,
'captype' => 'read',
'contextlevel' => CONTEXT_USER,
'archetypes' => [
'user' => CAP_ALLOW
]
],

// Capability for administrators to download other people's data requests.
'tool/dataprivacy:downloadallrequests' => [
'riskbitmask' => RISK_PERSONAL,
'captype' => 'read',
'contextlevel' => CONTEXT_USER,
'archetypes' => []
],
];
2 changes: 2 additions & 0 deletions admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
Expand Up @@ -66,6 +66,8 @@
$string['dataprivacy:makedatarequestsforchildren'] = 'Make data requests for minors';
$string['dataprivacy:managedatarequests'] = 'Manage data requests';
$string['dataprivacy:managedataregistry'] = 'Manage data registry';
$string['dataprivacy:downloadownrequest'] = 'Download your own exported data';
$string['dataprivacy:downloadallrequests'] = 'Download exported data for everyone';
$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['datarequestcreatedforuser'] = 'Data request created for {$a}';
Expand Down
24 changes: 8 additions & 16 deletions admin/tool/dataprivacy/lib.php
Expand Up @@ -185,26 +185,18 @@ function tool_dataprivacy_output_fragment_contextlevel_form($args) {
* @return bool Returns false if we don't find a file.
*/
function tool_dataprivacy_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options = array()) {
global $USER;

if ($context->contextlevel == CONTEXT_USER) {
// Make sure the user is logged in.
require_login(null, false);

// Validate the user downloading this archive.
$usercontext = context_user::instance($USER->id);
// The user downloading this is not the user the archive has been prepared for. Check if it's the requester (e.g. parent).
if ($usercontext->instanceid !== $context->instanceid) {
// Get the data request ID. This should be the first element of the $args array.
$itemid = $args[0];
// Fetch the data request object. An invalid ID will throw an exception.
$datarequest = new \tool_dataprivacy\data_request($itemid);

// Check if the user is the requester and has the capability to make data requests for the target user.
$candownloadforuser = has_capability('tool/dataprivacy:makedatarequestsforchildren', $context);
if ($USER->id != $datarequest->get('requestedby') || !$candownloadforuser) {
return false;
}
// Get the data request ID. This should be the first element of the $args array.
$itemid = $args[0];
// Fetch the data request object. An invalid ID will throw an exception.
$datarequest = new \tool_dataprivacy\data_request($itemid);

// Check if user is allowed to download it.
if (!\tool_dataprivacy\api::can_download_data_request_for_user($context->instanceid, $datarequest->get('requestedby'))) {
return false;
}

// All good. Serve the exported data.
Expand Down
51 changes: 51 additions & 0 deletions admin/tool/dataprivacy/tests/api_test.php
Expand Up @@ -276,6 +276,57 @@ public function test_can_manage_data_requests() {
$this->assertFalse(api::can_manage_data_requests($nondpoincapable->id));
}

/**
* Test for api::can_download_data_request_for_user()
*/
public function test_can_download_data_request_for_user() {
$generator = $this->getDataGenerator();

// Three victims.
$victim1 = $generator->create_user();
$victim2 = $generator->create_user();
$victim3 = $generator->create_user();

// Assign a user as victim 1's parent.
$systemcontext = \context_system::instance();
$parentrole = $generator->create_role();
assign_capability('tool/dataprivacy:makedatarequestsforchildren', CAP_ALLOW, $parentrole, $systemcontext);
$parent = $generator->create_user();
role_assign($parentrole, $parent->id, \context_user::instance($victim1->id));

// Assign another user as data access wonder woman.
$wonderrole = $generator->create_role();
assign_capability('tool/dataprivacy:downloadallrequests', CAP_ALLOW, $wonderrole, $systemcontext);
$staff = $generator->create_user();
role_assign($wonderrole, $staff->id, $systemcontext);

// Finally, victim 3 has been naughty; stop them accessing their own data.
$naughtyrole = $generator->create_role();
assign_capability('tool/dataprivacy:downloadownrequest', CAP_PROHIBIT, $naughtyrole, $systemcontext);
role_assign($naughtyrole, $victim3->id, $systemcontext);

// Victims 1 and 2 can access their own data, regardless of who requested it.
$this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $victim1->id));
$this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $victim2->id));

// Victim 3 cannot access his own data.
$this->assertFalse(api::can_download_data_request_for_user($victim3->id, $victim3->id, $victim3->id));

// Victims 1 and 2 cannot access another victim's data.
$this->assertFalse(api::can_download_data_request_for_user($victim2->id, $victim1->id, $victim1->id));
$this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $victim2->id));

// Staff can access everyone's data.
$this->assertTrue(api::can_download_data_request_for_user($victim1->id, $victim1->id, $staff->id));
$this->assertTrue(api::can_download_data_request_for_user($victim2->id, $staff->id, $staff->id));
$this->assertTrue(api::can_download_data_request_for_user($victim3->id, $staff->id, $staff->id));

// Parent can access victim 1's data only if they requested it.
$this->assertTrue(api::can_download_data_request_for_user($victim1->id, $parent->id, $parent->id));
$this->assertFalse(api::can_download_data_request_for_user($victim1->id, $staff->id, $parent->id));
$this->assertFalse(api::can_download_data_request_for_user($victim2->id, $parent->id, $parent->id));
}

/**
* Test for api::create_data_request()
*/
Expand Down

0 comments on commit 635c7b2

Please sign in to comment.