MDL-61652 tool_dataprivacy: Add capabilities to control data downloads

This commit is contained in:
sam marshall 2018-06-20 17:44:51 +01:00 committed by Mihail Geshoski
parent 0180369b27
commit 635c7b29a0
10 changed files with 289 additions and 30 deletions

View File

@ -608,6 +608,54 @@ class api {
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.
*

View File

@ -208,6 +208,14 @@ class data_requests_table extends table_sql {
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);

View File

@ -95,7 +95,8 @@ class my_data_requests_page implements renderable, templatable {
$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);
@ -110,6 +111,10 @@ class my_data_requests_page implements renderable, templatable {
$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:
@ -126,10 +131,7 @@ class my_data_requests_page implements renderable, templatable {
$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);

View File

@ -139,8 +139,17 @@ class process_data_request_task extends adhoc_task {
$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;
@ -179,18 +188,40 @@ class process_data_request_task extends adhoc_task {
$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);

View File

@ -49,4 +49,22 @@ $capabilities = [
'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' => []
],
];

View File

@ -66,6 +66,8 @@ $string['datadeletionpagehelp'] = 'Data for which the retention period has expir
$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}';

View File

@ -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);
// 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;
}
// 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.

View File

@ -276,6 +276,57 @@ class tool_dataprivacy_api_testcase extends advanced_testcase {
$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()
*/

View File

@ -0,0 +1,107 @@
@tool @tool_dataprivacy
Feature: Data export from the privacy API
In order to export data for users and meet legal requirements
As an admin, user, or parent
I need to be able to export data for a user
Background:
Given the following "users" exist:
| username | firstname | lastname |
| victim | Victim User | 1 |
| parent | Long-suffering | Parent |
And the following "roles" exist:
| shortname | name | archetype |
| tired | Tired | |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| tool/dataprivacy:makedatarequestsforchildren | Allow | tired | System | |
And the following "role assigns" exist:
| user | role | contextlevel | reference |
| parent | tired | User | victim |
And the following config values are set as admin:
| contactdataprotectionofficer | 1 | tool_dataprivacy |
@javascript
Scenario: As admin, export data for a user and download it
Given I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "New request"
And I set the field "Requesting for" to "Victim User 1"
And I press "Save changes"
Then I should see "Victim User 1"
And I should see "Pending" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
And I follow "Actions"
And I follow "Approve request"
And I press "Approve request"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Complete" in the "Victim User 1" "table_row"
And I follow "Actions"
And following "Download" should download between "1" and "100000" bytes
@javascript
Scenario: As a student, request data export and then download it when approved
Given I log in as "victim"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I follow "New request"
And I press "Save changes"
Then I should see "Export all of my personal data"
And I should see "Pending" in the "Export all of my personal data" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Awaiting approval" in the "Export all of my personal data" "table_row"
And I log out
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "Actions"
And I follow "Approve request"
And I press "Approve request"
And I log out
And I log in as "victim"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I should see "Approved" in the "Export all of my personal data" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Complete" in the "Export all of my personal data" "table_row"
And I follow "Actions"
And following "Download" should download between "1" and "100000" bytes
@javascript
Scenario: As a parent, request data export for my child because I don't trust the little blighter
Given I log in as "parent"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I follow "New request"
And I set the field "Requesting for" to "Victim User 1"
And I press "Save changes"
Then I should see "Victim User 1"
And I should see "Pending" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Awaiting approval" in the "Victim User 1" "table_row"
And I log out
And I log in as "admin"
And I navigate to "Users > Privacy and policies > Data requests" in site administration
And I follow "Actions"
And I follow "Approve request"
And I press "Approve request"
And I log out
And I log in as "parent"
And I follow "Profile" in the user menu
And I follow "Data requests"
And I should see "Approved" in the "Victim User 1" "table_row"
And I run all adhoc tasks
And I reload the page
And I should see "Complete" in the "Victim User 1" "table_row"
And I follow "Actions"
And following "Download" should download between "1" and "100000" bytes

View File

@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die;
$plugin->version = 2018051402;
$plugin->version = 2018051403;
$plugin->requires = 2018050800; // Moodle 3.5dev (Build 2018031600) and upwards.
$plugin->component = 'tool_dataprivacy';