diff --git a/admin/tool/dataprivacy/classes/api.php b/admin/tool/dataprivacy/classes/api.php index 9ae86472274..906dcb5efba 100644 --- a/admin/tool/dataprivacy/classes/api.php +++ b/admin/tool/dataprivacy/classes/api.php @@ -258,7 +258,20 @@ class api { // The user making the request. $datarequest->set('requestedby', $requestinguser); // Set status. - $datarequest->set('status', self::DATAREQUEST_STATUS_AWAITING_APPROVAL); + $status = self::DATAREQUEST_STATUS_AWAITING_APPROVAL; + if (self::is_automatic_request_approval_on($type)) { + // Set status to approved if automatic data request approval is enabled. + $status = self::DATAREQUEST_STATUS_APPROVED; + // Set the privacy officer field if the one making the data request is a privacy officer. + if (self::is_site_dpo($requestinguser)) { + $datarequest->set('dpo', $requestinguser); + } + // Mark this request as system approved. + $datarequest->set('systemapproved', true); + // No need to notify privacy officer(s) about automatically approved data requests. + $notify = false; + } + $datarequest->set('status', $status); // Set request type. $datarequest->set('type', $type); // Set request comments. @@ -269,13 +282,22 @@ class api { // Store subject access request. $datarequest->create(); + // Queue the ad-hoc task for automatically approved data requests. + if ($status == self::DATAREQUEST_STATUS_APPROVED) { + $userid = null; + if ($type == self::DATAREQUEST_TYPE_EXPORT) { + $userid = $foruser; + } + self::queue_data_request_task($datarequest->get('id'), $userid); + } + if ($notify) { // Get the list of the site Data Protection Officers. - $dpos = api::get_site_dpos(); + $dpos = self::get_site_dpos(); // Email the data request to the Data Protection Officer(s)/Admin(s). foreach ($dpos as $dpo) { - api::notify_dpo($dpo, $datarequest); + self::notify_dpo($dpo, $datarequest); } } @@ -624,12 +646,11 @@ class api { $result = self::update_request_status($requestid, self::DATAREQUEST_STATUS_APPROVED, $USER->id); // Fire an ad hoc task to initiate the data request process. - $task = new process_data_request_task(); - $task->set_custom_data(['requestid' => $requestid]); + $userid = null; if ($request->get('type') == self::DATAREQUEST_TYPE_EXPORT) { - $task->set_userid($request->get('userid')); + $userid = $request->get('userid'); } - manager::queue_adhoc_task($task, true); + self::queue_data_request_task($requestid, $userid); return $result; } @@ -1277,4 +1298,35 @@ class api { return $formattedtime; } + + /** + * Whether automatic data request approval is turned on or not for the given request type. + * + * @param int $type The request type. + * @return bool + */ + public static function is_automatic_request_approval_on(int $type): bool { + switch ($type) { + case self::DATAREQUEST_TYPE_EXPORT: + return !empty(get_config('tool_dataprivacy', 'automaticdataexportapproval')); + case self::DATAREQUEST_TYPE_DELETE: + return !empty(get_config('tool_dataprivacy', 'automaticdatadeletionapproval')); + } + return false; + } + + /** + * Creates an ad-hoc task for the data request. + * + * @param int $requestid The data request ID. + * @param int $userid Optional. The user ID to run the task as, if necessary. + */ + public static function queue_data_request_task(int $requestid, int $userid = null): void { + $task = new process_data_request_task(); + $task->set_custom_data(['requestid' => $requestid]); + if ($userid) { + $task->set_userid($userid); + } + manager::queue_adhoc_task($task, true); + } } diff --git a/admin/tool/dataprivacy/classes/data_request.php b/admin/tool/dataprivacy/classes/data_request.php index 39015632a5f..6a94fad41a1 100644 --- a/admin/tool/dataprivacy/classes/data_request.php +++ b/admin/tool/dataprivacy/classes/data_request.php @@ -118,6 +118,10 @@ class data_request extends persistent { 'type' => PARAM_INT, 'default' => FORMAT_PLAIN ], + 'systemapproved' => [ + 'default' => false, + 'type' => PARAM_BOOL, + ], 'creationmethod' => [ 'default' => self::DATAREQUEST_CREATION_MANUAL, 'choices' => [ diff --git a/admin/tool/dataprivacy/classes/task/process_data_request_task.php b/admin/tool/dataprivacy/classes/task/process_data_request_task.php index 780e25afa89..bb29c6c59bc 100644 --- a/admin/tool/dataprivacy/classes/task/process_data_request_task.php +++ b/admin/tool/dataprivacy/classes/task/process_data_request_task.php @@ -146,14 +146,20 @@ class process_data_request_task extends adhoc_task { mtrace('The processing of the user data request has been completed...'); // Create message to notify the user regarding the processing results. - $dpo = core_user::get_user($request->dpo); $message = new message(); $message->courseid = $SITE->id; $message->component = 'tool_dataprivacy'; $message->name = 'datarequestprocessingresults'; - $message->userfrom = $dpo; - $message->replyto = $dpo->email; - $message->replytoname = fullname($dpo); + if (empty($request->dpo)) { + // Use the no-reply user as the sender if the privacy officer is not set. This is the case for automatically + // approved requests. + $fromuser = core_user::get_noreply_user(); + } else { + $fromuser = core_user::get_user($request->dpo); + $message->replyto = $fromuser->email; + $message->replytoname = fullname($fromuser); + } + $message->userfrom = $fromuser; $typetext = null; // Prepare the context data for the email message body. @@ -219,7 +225,7 @@ class process_data_request_task extends adhoc_task { if ($emailonly) { // Do not sent an email if the user has been deleted. The user email has been previously deleted. if (!$foruser->deleted) { - $messagesent = email_to_user($foruser, $dpo, $subject, $message->fullmessage, $messagehtml); + $messagesent = email_to_user($foruser, $fromuser, $subject, $message->fullmessage, $messagehtml); } } else { $messagesent = message_send($message); @@ -265,7 +271,7 @@ class process_data_request_task extends adhoc_task { // Send message. if ($emailonly) { - email_to_user($requestedby, $dpo, $subject, $message->fullmessage, $messagehtml); + email_to_user($requestedby, $fromuser, $subject, $message->fullmessage, $messagehtml); } else { message_send($message); } diff --git a/admin/tool/dataprivacy/createdatarequest.php b/admin/tool/dataprivacy/createdatarequest.php index b32dc159879..2124400804c 100644 --- a/admin/tool/dataprivacy/createdatarequest.php +++ b/admin/tool/dataprivacy/createdatarequest.php @@ -94,7 +94,11 @@ if ($data = $mform->get_data()) { if ($manage) { $foruser = core_user::get_user($data->userid); $redirectmessage = get_string('datarequestcreatedforuser', 'tool_dataprivacy', fullname($foruser)); + } else if (\tool_dataprivacy\api::is_automatic_request_approval_on($data->type)) { + // Let the user know that the request has been submitted and will be processed soon. + $redirectmessage = get_string('approvedrequestsubmitted', 'tool_dataprivacy'); } else { + // Let the user know that the request has been submitted to the privacy officer. $redirectmessage = get_string('requestsubmitted', 'tool_dataprivacy'); } redirect($returnurl, $redirectmessage); diff --git a/admin/tool/dataprivacy/db/install.xml b/admin/tool/dataprivacy/db/install.xml index fc5e96b3390..a7b564c63b2 100644 --- a/admin/tool/dataprivacy/db/install.xml +++ b/admin/tool/dataprivacy/db/install.xml @@ -1,5 +1,5 @@ - @@ -16,6 +16,7 @@ + diff --git a/admin/tool/dataprivacy/db/upgrade.php b/admin/tool/dataprivacy/db/upgrade.php index 7b2a9d8bc32..e90e82a68df 100644 --- a/admin/tool/dataprivacy/db/upgrade.php +++ b/admin/tool/dataprivacy/db/upgrade.php @@ -322,5 +322,20 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) { // Automatically generated Moodle v3.8.0 release upgrade line. // Put any upgrade step following this. + if ($oldversion < 2019121700) { + + // Define field systemapproved to be added to tool_dataprivacy_request. + $table = new xmldb_table('tool_dataprivacy_request'); + $field = new xmldb_field('systemapproved', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '0', 'dpocommentformat'); + + // Conditionally launch add field systemapproved. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Dataprivacy savepoint reached. + upgrade_plugin_savepoint(true, 2019121700, 'tool', 'dataprivacy'); + } + return true; } diff --git a/admin/tool/dataprivacy/lang/en/tool_dataprivacy.php b/admin/tool/dataprivacy/lang/en/tool_dataprivacy.php index 5a0efd41ae9..9a317581da9 100644 --- a/admin/tool/dataprivacy/lang/en/tool_dataprivacy.php +++ b/admin/tool/dataprivacy/lang/en/tool_dataprivacy.php @@ -31,7 +31,12 @@ $string['addcategory'] = 'Add category'; $string['addnewdefaults'] = 'Add a new module default'; $string['addpurpose'] = 'Add purpose'; $string['approve'] = 'Approve'; +$string['approvedrequestsubmitted'] = 'Your request has been submitted and will be processed soon'; $string['approverequest'] = 'Approve request'; +$string['automaticdatadeletionapproval'] = 'Automatic data deletion request approval'; +$string['automaticdatadeletionapproval_desc'] = 'If enabled, data deletion requests are automatically approved.
Note that the automatic approval will only apply to new data deletion requests with this setting enabled. Existing data deletion requests pending approval will still have to be manually approved by the privacy officer.'; +$string['automaticdataexportapproval'] = 'Automatic data export request approval'; +$string['automaticdataexportapproval_desc'] = 'If enabled, data export requests are automatically approved.
Note that the automatic approval will only apply to new data export requests with this setting enabled. Existing data export requests pending approval will still have to be manually approved by the privacy officer.'; $string['automaticdeletionrequests'] = 'Create automatic data deletion requests'; $string['automaticdeletionrequests_desc'] = 'If enabled, a data deletion request will be created automatically for any user accounts deleted manually.'; $string['bulkapproverequests'] = 'Approve requests'; diff --git a/admin/tool/dataprivacy/settings.php b/admin/tool/dataprivacy/settings.php index de3d7151ecb..0a335e33809 100644 --- a/admin/tool/dataprivacy/settings.php +++ b/admin/tool/dataprivacy/settings.php @@ -34,6 +34,16 @@ if ($hassiteconfig) { new lang_string('contactdataprotectionofficer_desc', 'tool_dataprivacy'), 0) ); + $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/automaticdataexportapproval', + new lang_string('automaticdataexportapproval', 'tool_dataprivacy'), + new lang_string('automaticdataexportapproval_desc', 'tool_dataprivacy'), 0) + ); + + $privacysettings->add(new admin_setting_configcheckbox('tool_dataprivacy/automaticdatadeletionapproval', + new lang_string('automaticdatadeletionapproval', 'tool_dataprivacy'), + new lang_string('automaticdatadeletionapproval_desc', 'tool_dataprivacy'), 0) + ); + // Automatically create delete data request for users upon user deletion. // Automatically create delete data request for pre-existing deleted users. // Enabled by default. diff --git a/admin/tool/dataprivacy/tests/api_test.php b/admin/tool/dataprivacy/tests/api_test.php index a913f3ec1d2..d86d17743c1 100644 --- a/admin/tool/dataprivacy/tests/api_test.php +++ b/admin/tool/dataprivacy/tests/api_test.php @@ -550,32 +550,65 @@ class tool_dataprivacy_api_testcase extends advanced_testcase { } /** - * Test for api::create_data_request() + * Data provider for data request creation tests. + * + * @return array */ - public function test_create_data_request() { - $this->resetAfterTest(); - - $generator = new testing_data_generator(); - $user = $generator->create_user(); - $comment = 'sample comment'; - - // Login as user. - $this->setUser($user->id); - - // Test data request creation. - $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment); - $this->assertEquals($user->id, $datarequest->get('userid')); - $this->assertEquals($user->id, $datarequest->get('requestedby')); - $this->assertEquals(0, $datarequest->get('dpo')); - $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type')); - $this->assertEquals(api::DATAREQUEST_STATUS_AWAITING_APPROVAL, $datarequest->get('status')); - $this->assertEquals($comment, $datarequest->get('comments')); + public function data_request_creation_provider() { + return [ + 'Export request by user, automatic approval off' => [ + false, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', false, 0, + api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0 + ], + 'Export request by user, automatic approval on' => [ + false, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', true, 0, + api::DATAREQUEST_STATUS_APPROVED, 1 + ], + 'Export request by PO, automatic approval off' => [ + true, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', false, 0, + api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0 + ], + 'Export request by PO, automatic approval on' => [ + true, api::DATAREQUEST_TYPE_EXPORT, 'automaticdataexportapproval', true, 'dpo', + api::DATAREQUEST_STATUS_APPROVED, 1 + ], + 'Delete request by user, automatic approval off' => [ + false, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', false, 0, + api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0 + ], + 'Delete request by user, automatic approval on' => [ + false, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', true, 0, + api::DATAREQUEST_STATUS_APPROVED, 1 + ], + 'Delete request by PO, automatic approval off' => [ + true, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', false, 0, + api::DATAREQUEST_STATUS_AWAITING_APPROVAL, 0 + ], + 'Delete request by PO, automatic approval on' => [ + true, api::DATAREQUEST_TYPE_DELETE, 'automaticdatadeletionapproval', true, 'dpo', + api::DATAREQUEST_STATUS_APPROVED, 1 + ], + ]; } /** - * Test for api::create_data_request() made by DPO. + * Test for api::create_data_request() + * + * @dataProvider data_request_creation_provider + * @param bool $asprivacyofficer Whether the request is made as the Privacy Officer or the user itself. + * @param string $type The data request type. + * @param string $setting The automatic approval setting. + * @param bool $automaticapproval Whether automatic data request approval is turned on or not. + * @param int|string $expecteddpoval The expected value for the 'dpo' field. 'dpo' means we'd the expected value would be the + * user ID of the privacy officer which happens in the case where a PO requests on behalf of + * someone else and automatic data request approval is turned on. + * @param int $expectedstatus The expected status of the data request. + * @param int $expectedtaskcount The number of expected queued data requests tasks. + * @throws coding_exception + * @throws invalid_persistent_exception */ - public function test_create_data_request_by_dpo() { + public function test_create_data_request($asprivacyofficer, $type, $setting, $automaticapproval, $expecteddpoval, + $expectedstatus, $expectedtaskcount) { global $USER; $this->resetAfterTest(); @@ -584,16 +617,34 @@ class tool_dataprivacy_api_testcase extends advanced_testcase { $user = $generator->create_user(); $comment = 'sample comment'; - // Login as DPO (Admin is DPO by default). - $this->setAdminUser(); + // Login. + if ($asprivacyofficer) { + $this->setAdminUser(); + } else { + $this->setUser($user->id); + } + + // Set the automatic data request approval setting value. + set_config($setting, $automaticapproval, 'tool_dataprivacy'); + + // If set to 'dpo' use the currently logged-in user's ID (which should be the admin user's ID). + if ($expecteddpoval === 'dpo') { + $expecteddpoval = $USER->id; + } // Test data request creation. - $datarequest = api::create_data_request($user->id, api::DATAREQUEST_TYPE_EXPORT, $comment); + $datarequest = api::create_data_request($user->id, $type, $comment); $this->assertEquals($user->id, $datarequest->get('userid')); $this->assertEquals($USER->id, $datarequest->get('requestedby')); - $this->assertEquals(api::DATAREQUEST_TYPE_EXPORT, $datarequest->get('type')); - $this->assertEquals(api::DATAREQUEST_STATUS_AWAITING_APPROVAL, $datarequest->get('status')); + $this->assertEquals($expecteddpoval, $datarequest->get('dpo')); + $this->assertEquals($type, $datarequest->get('type')); + $this->assertEquals($expectedstatus, $datarequest->get('status')); $this->assertEquals($comment, $datarequest->get('comments')); + $this->assertEquals($automaticapproval, $datarequest->get('systemapproved')); + + // Test number of queued data request tasks. + $datarequesttasks = manager::get_adhoc_tasks(process_data_request_task::class); + $this->assertCount($expectedtaskcount, $datarequesttasks); } /** @@ -2250,4 +2301,88 @@ class tool_dataprivacy_api_testcase extends advanced_testcase { set_config('siteadmins', $child->id); $this->assertFalse(api::can_create_data_deletion_request_for_children($child->id)); } + + /** + * Data provider function for testing \tool_dataprivacy\api::queue_data_request_task(). + * + * @return array + */ + public function queue_data_request_task_provider() { + return [ + 'With user ID provided' => [true], + 'Without user ID provided' => [false], + ]; + } + + /** + * Test for \tool_dataprivacy\api::queue_data_request_task(). + * + * @dataProvider queue_data_request_task_provider + * @param bool $withuserid + */ + public function test_queue_data_request_task(bool $withuserid) { + $this->resetAfterTest(); + + $this->setAdminUser(); + + if ($withuserid) { + $user = $this->getDataGenerator()->create_user(); + api::queue_data_request_task(1, $user->id); + $expecteduserid = $user->id; + } else { + api::queue_data_request_task(1); + $expecteduserid = null; + } + + // Test number of queued data request tasks. + $datarequesttasks = manager::get_adhoc_tasks(process_data_request_task::class); + $this->assertCount(1, $datarequesttasks); + $requesttask = reset($datarequesttasks); + $this->assertEquals($expecteduserid, $requesttask->get_userid()); + } + + /** + * Data provider for test_is_automatic_request_approval_on(). + */ + public function automatic_request_approval_setting_provider() { + return [ + 'Data export, not set' => [ + 'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, null, false + ], + 'Data export, turned on' => [ + 'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, true, true + ], + 'Data export, turned off' => [ + 'automaticdataexportapproval', api::DATAREQUEST_TYPE_EXPORT, false, false + ], + 'Data deletion, not set' => [ + 'automaticdatadeletionapproval', api::DATAREQUEST_TYPE_DELETE, null, false + ], + 'Data deletion, turned on' => [ + 'automaticdatadeletionapproval', api::DATAREQUEST_TYPE_DELETE, true, true + ], + 'Data deletion, turned off' => [ + 'automaticdatadeletionapproval', api::DATAREQUEST_TYPE_DELETE, false, false + ], + ]; + } + + /** + * Test for \tool_dataprivacy\api::is_automatic_request_approval_on(). + * + * @dataProvider automatic_request_approval_setting_provider + * @param string $setting The automatic approval setting. + * @param int $type The data request type. + * @param bool $value The setting's value. + * @param bool $expected The expected result. + */ + public function test_is_automatic_request_approval_on($setting, $type, $value, $expected) { + $this->resetAfterTest(); + + if ($value !== null) { + set_config($setting, $value, 'tool_dataprivacy'); + } + + $this->assertEquals($expected, api::is_automatic_request_approval_on($type)); + } } diff --git a/admin/tool/dataprivacy/tests/behat/datadelete.feature b/admin/tool/dataprivacy/tests/behat/datadelete.feature index 0a4da1b114f..4add17ff96e 100644 --- a/admin/tool/dataprivacy/tests/behat/datadelete.feature +++ b/admin/tool/dataprivacy/tests/behat/datadelete.feature @@ -240,3 +240,13 @@ Feature: Data delete from the privacy API And I reload the page And I open the action menu in "region-main" "region" Then I should not see "Resubmit as new request" + + Scenario: Request data deletion as student with automatic approval turned on + Given the following config values are set as admin: + | automaticdatadeletionapproval | 1 | tool_dataprivacy | + And I log in as "victim" + And I follow "Profile" in the user menu + And I follow "Delete my account" + When I press "Save changes" + Then I should see "Your request has been submitted and will be processed soon" + And I should see "Approved" in the "Delete all of my personal data" "table_row" diff --git a/admin/tool/dataprivacy/tests/behat/dataexport.feature b/admin/tool/dataprivacy/tests/behat/dataexport.feature index b651201be77..65fec34ae42 100644 --- a/admin/tool/dataprivacy/tests/behat/dataexport.feature +++ b/admin/tool/dataprivacy/tests/behat/dataexport.feature @@ -151,3 +151,13 @@ Feature: Data export from the privacy API When I reload the page And I set the field "Search" to "University2" Then I should see "Victim User 2" + + Scenario: Request data export as student with automatic approval turned on + Given the following config values are set as admin: + | automaticdataexportapproval | 1 | tool_dataprivacy | + And I log in as "victim" + And I follow "Profile" in the user menu + And I follow "Export all of my personal data" + When I press "Save changes" + Then I should see "Your request has been submitted and will be processed soon" + And I should see "Approved" in the "Export all of my personal data" "table_row" diff --git a/admin/tool/dataprivacy/version.php b/admin/tool/dataprivacy/version.php index 636f744cbf9..5242cdf2789 100644 --- a/admin/tool/dataprivacy/version.php +++ b/admin/tool/dataprivacy/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die; -$plugin->version = 2019111800; +$plugin->version = 2019121700; $plugin->requires = 2019111200; // Moodle 3.5dev (Build 2018031600) and upwards. $plugin->component = 'tool_dataprivacy';