mirror of
https://github.com/moodle/moodle.git
synced 2025-03-23 17:10:20 +01:00
MDL-61652 tool_dataprivacy: Add capabilities to control data downloads
This commit is contained in:
parent
0180369b27
commit
635c7b29a0
@ -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.
|
||||
*
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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' => []
|
||||
],
|
||||
];
|
||||
|
@ -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}';
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
*/
|
||||
|
107
admin/tool/dataprivacy/tests/behat/dataexport.feature
Normal file
107
admin/tool/dataprivacy/tests/behat/dataexport.feature
Normal 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
|
@ -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';
|
||||
|
Loading…
x
Reference in New Issue
Block a user