MDL-66752 tool_dataprivacy: Add automatic data request approval feature
authorJun Pataleta <jun@moodle.com>
Thu, 5 Dec 2019 14:17:18 +0000 (22:17 +0800)
committerJun Pataleta <jun@moodle.com>
Mon, 6 Jan 2020 03:40:31 +0000 (11:40 +0800)
12 files changed:
admin/tool/dataprivacy/classes/api.php
admin/tool/dataprivacy/classes/data_request.php
admin/tool/dataprivacy/classes/task/process_data_request_task.php
admin/tool/dataprivacy/createdatarequest.php
admin/tool/dataprivacy/db/install.xml
admin/tool/dataprivacy/db/upgrade.php
admin/tool/dataprivacy/lang/en/tool_dataprivacy.php
admin/tool/dataprivacy/settings.php
admin/tool/dataprivacy/tests/api_test.php
admin/tool/dataprivacy/tests/behat/datadelete.feature
admin/tool/dataprivacy/tests/behat/dataexport.feature
admin/tool/dataprivacy/version.php

index 9ae8647..906dcb5 100644 (file)
@@ -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);
+    }
 }
index 3901563..6a94fad 100644 (file)
@@ -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' => [
index 780e25a..bb29c6c 100644 (file)
@@ -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);
                 }
index b32dc15..2124400 100644 (file)
@@ -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);
index fc5e96b..a7b564c 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20181107" COMMENT="XMLDB file for Moodle tool/dataprivacy"
+<XMLDB PATH="admin/tool/dataprivacy/db" VERSION="20191217" COMMENT="XMLDB file for Moodle tool/dataprivacy"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../../../lib/xmldb/xmldb.xsd"
 >
@@ -16,6 +16,7 @@
         <FIELD NAME="dpo" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the Data Protection Officer who is reviewing th request"/>
         <FIELD NAME="dpocomment" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="DPO's comments (e.g. reason for rejecting the request, etc.)"/>
         <FIELD NAME="dpocommentformat" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
+        <FIELD NAME="systemapproved" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
         <FIELD NAME="usermodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user who created/modified this request object"/>
         <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time this data request was created"/>
         <FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The last time this data request was updated"/>
index 7b2a9d8..e90e82a 100644 (file)
@@ -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;
 }
index 5a0efd4..9a31758 100644 (file)
@@ -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.<br/>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.<br/>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';
index de3d715..0a335e3 100644 (file)
@@ -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.
index a913f3e..d86d177 100644 (file)
@@ -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));
+    }
 }
index 0a4da1b..4add17f 100644 (file)
@@ -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"
index b651201..65fec34 100644 (file)
@@ -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"
index 636f744..5242cdf 100644 (file)
@@ -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';