diff --git a/repository/onedrive/classes/privacy/provider.php b/repository/onedrive/classes/privacy/provider.php new file mode 100644 index 0000000000000..27f830425b1b8 --- /dev/null +++ b/repository/onedrive/classes/privacy/provider.php @@ -0,0 +1,199 @@ +. + +/** + * Privacy Subsystem implementation for repository_onedrive. + * + * @package repository_onedrive + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace repository_onedrive\privacy; + +use core_privacy\local\metadata\collection; +use core_privacy\local\request\approved_contextlist; +use core_privacy\local\request\context; +use core_privacy\local\request\contextlist; +use core_privacy\local\request\transform; +use \core_privacy\local\request\writer; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Privacy Subsystem for repository_onedrive implementing metadata and plugin providers. + * + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\plugin\provider { + + /** + * Returns meta data about this system. + * + * @param collection $collection The initialised collection to add items to. + * @return collection A listing of user data stored through this system. + */ + public static function get_metadata(collection $collection) : collection { + $collection->add_external_location_link( + 'onedrive.live.com', + [ + 'searchtext' => 'privacy:metadata:repository_onedrive:searchtext' + ], + 'privacy:metadata:repository_onedrive' + ); + + // The repository_onedrive has a 'repository_onedrive_access' table that contains user data. + $collection->add_database_table( + 'repository_onedrive_access', + [ + 'itemid' => 'privacy:metadata:repository_onedrive:repository_onedrive_access:itemid', + 'permissionid' => 'privacy:metadata:repository_onedrive:repository_onedrive_access:permissionid', + 'timecreated' => 'privacy:metadata:repository_onedrive:repository_onedrive_access:timecreated', + 'timemodified' => 'privacy:metadata:repository_onedrive:repository_onedrive_access:timemodified', + 'usermodified' => 'privacy:metadata:repository_onedrive:repository_onedrive_access:usermodified' + ], + 'privacy:metadata:repository_onedrive' + ); + + return $collection; + } + + /** + * Get the list of contexts that contain user information for the specified user. + * + * @param int $userid The user to search. + * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. + */ + public static function get_contexts_for_userid(int $userid) : contextlist { + $contextlist = new contextlist(); + + // The data is associated at the user context level, so retrieve the user's context id. + $sql = "SELECT c.id + FROM {repository_onedrive_access} roa + JOIN {context} c ON c.instanceid = roa.usermodified AND c.contextlevel = :contextuser + WHERE roa.usermodified = :userid + GROUP BY c.id"; + + $params = [ + 'contextuser' => CONTEXT_USER, + 'userid' => $userid + ]; + + $contextlist->add_from_sql($sql, $params); + + return $contextlist; + } + + /** + * Export all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts to export information for. + */ + public static function export_user_data(approved_contextlist $contextlist) { + global $DB; + + // If the user has data, then only the User context should be present so get the first context. + $contexts = $contextlist->get_contexts(); + if (count($contexts) == 0) { + return; + } + $context = reset($contexts); + + // Sanity check that context is at the User context level, then get the userid. + if ($context->contextlevel !== CONTEXT_USER) { + return; + } + $userid = $context->instanceid; + + $sql = "SELECT roa.id as id, + roa.itemid as itemid, + roa.permissionid as permissionid, + roa.timecreated as timecreated, + roa.timemodified as timemodified + FROM {repository_onedrive_access} roa + WHERE roa.usermodified = :userid"; + + $params = [ + 'userid' => $userid + ]; + + $onedriveaccesses = $DB->get_records_sql($sql, $params); + $index = 0; + foreach ($onedriveaccesses as $onedriveaccess) { + // Data export is organised in: {User Context}/Repository plug-ins/{Plugin Name}/Access/{index}/data.json. + $index++; + $subcontext = [ + get_string('plugin', 'core_repository'), + get_string('pluginname', 'repository_onedrive'), + get_string('access', 'repository_onedrive'), + $index + ]; + + $data = (object) [ + 'itemid' => $onedriveaccess->itemid, + 'permissionid' => $onedriveaccess->permissionid, + 'timecreated' => transform::datetime($onedriveaccess->timecreated), + 'timemodified' => transform::datetime($onedriveaccess->timemodified) + ]; + + writer::with_context($context)->export_data($subcontext, $data); + } + + } + + /** + * Delete all data for all users in the specified context. + * + * @param context $context The specific context to delete data for. + */ + public static function delete_data_for_all_users_in_context(\context $context) { + global $DB; + + // Sanity check that context is at the User context level, then get the userid. + if ($context->contextlevel !== CONTEXT_USER) { + return; + } + $userid = $context->instanceid; + + $DB->delete_records('repository_onedrive_access', ['usermodified' => $userid]); + } + + /** + * Delete all user data for the specified user, in the specified contexts. + * + * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. + */ + public static function delete_data_for_user(approved_contextlist $contextlist) { + global $DB; + + // If the user has data, then only the User context should be present so get the first context. + $contexts = $contextlist->get_contexts(); + if (count($contexts) == 0) { + return; + } + $context = reset($contexts); + + // Sanity check that context is at the User context level, then get the userid. + if ($context->contextlevel !== CONTEXT_USER) { + return; + } + $userid = $context->instanceid; + + $DB->delete_records('repository_onedrive_access', ['usermodified' => $userid]); + } + +} diff --git a/repository/onedrive/lang/en/repository_onedrive.php b/repository/onedrive/lang/en/repository_onedrive.php index 83ab88afde689..dfa298a28689f 100644 --- a/repository/onedrive/lang/en/repository_onedrive.php +++ b/repository/onedrive/lang/en/repository_onedrive.php @@ -23,6 +23,7 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['access'] = 'Access'; $string['both'] = 'Internal and external'; $string['cachedef_folder'] = 'OneDrive file IDs for folders in the system account'; $string['configplugin'] = 'Configure OneDrive plugin'; @@ -46,3 +47,10 @@ $string['skydrivefilesnotimported'] = 'Some files could not be imported from the Microsoft SkyDrive repository.'; $string['onedrive:view'] = 'View OneDrive repository'; $string['supportedreturntypes'] = 'Supported files'; +$string['privacy:metadata:repository_onedrive'] = 'The Microsoft OneDrive repository stores temporary access grants, and transmits user data from Moodle to the remote system.'; +$string['privacy:metadata:repository_onedrive:searchtext'] = 'The Microsoft OneDrive repository user search text query.'; +$string['privacy:metadata:repository_onedrive:repository_onedrive_access:itemid'] = 'The Microsoft OneDrive with a temporary access grant item ID.'; +$string['privacy:metadata:repository_onedrive:repository_onedrive_access:permissionid'] = 'The Microsoft OneDrive temporary access grant permission ID.'; +$string['privacy:metadata:repository_onedrive:repository_onedrive_access:timecreated'] = 'The Microsoft OneDrive temporary access grant creation date/time.'; +$string['privacy:metadata:repository_onedrive:repository_onedrive_access:timemodified'] = 'The Microsoft OneDrive temporary access grant modification date/time.'; +$string['privacy:metadata:repository_onedrive:repository_onedrive_access:usermodified'] = 'The ID of the user modifying the Microsoft OneDrive temporary access grant.'; diff --git a/repository/onedrive/tests/privacy_test.php b/repository/onedrive/tests/privacy_test.php new file mode 100644 index 0000000000000..d31bffb01dc72 --- /dev/null +++ b/repository/onedrive/tests/privacy_test.php @@ -0,0 +1,256 @@ +. +/** + * Unit tests for the repository_onedrive implementation of the privacy API. + * + * @package repository_onedrive + * @category test + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +defined('MOODLE_INTERNAL') || die(); +use \core_privacy\local\metadata\collection; +use \core_privacy\local\request\writer; +use \core_privacy\local\request\approved_contextlist; +use \repository_onedrive\privacy\provider; +/** + * Unit tests for the repository_onedrive implementation of the privacy API. + * + * @copyright 2018 Zig Tan + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class repository_onedrive_privacy_testcase extends \core_privacy\tests\provider_testcase { + + /** + * Overriding setUp() function to always reset after tests. + */ + public function setUp() { + $this->resetAfterTest(true); + } + + /** + * Test for provider::get_contexts_for_userid(). + */ + public function test_get_contexts_for_userid() { + global $DB; + + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Add two repository_onedrive_access records for the User. + $access = (object)[ + 'usermodified' => $user->id, + 'itemid' => 'Some onedrive access item data', + 'permissionid' => 'Some onedrive access permission data', + 'timecreated' => date('u'), + 'timemodified' => date('u'), + ]; + $DB->insert_record('repository_onedrive_access', $access); + $access = (object)[ + 'usermodified' => $user->id, + 'itemid' => 'Another onedrive access item data', + 'permissionid' => 'Another onedrive access permission data', + 'timecreated' => date('u'), + 'timemodified' => date('u'), + ]; + $DB->insert_record('repository_onedrive_access', $access); + + // Test there are two repository_onedrive_access records for the User. + $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user->id]); + $this->assertCount(2, $access); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($user->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + $this->assertEquals($user->id, $context->instanceid); + } + + /** + * Test for provider::export_user_data(). + */ + public function test_export_user_data() { + global $DB; + + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Add two repository_onedrive_access records for the User. + $access = (object)[ + 'usermodified' => $user->id, + 'itemid' => 'Some onedrive access item data', + 'permissionid' => 'Some onedrive access permission data', + 'timecreated' => date('u'), + 'timemodified' => date('u'), + ]; + $DB->insert_record('repository_onedrive_access', $access); + $access = (object)[ + 'usermodified' => $user->id, + 'itemid' => 'Another onedrive access item data', + 'permissionid' => 'Another onedrive access permission data', + 'timecreated' => date('u'), + 'timemodified' => date('u'), + ]; + $DB->insert_record('repository_onedrive_access', $access); + + // Test there are two repository_onedrive_access records for the User. + $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user->id]); + $this->assertCount(2, $access); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($user->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + $this->assertEquals($user->id, $context->instanceid); + $approvedcontextlist = new approved_contextlist($user, 'repository_onedrive', $contextlist->get_contextids()); + + // Retrieve repository_onedrive_access data only for this user. + provider::export_user_data($approvedcontextlist); + + // Test the repository_onedrive_access data is exported at the User context level. + $user = $approvedcontextlist->get_user(); + $contextuser = context_user::instance($user->id); + $writer = writer::with_context($contextuser); + $this->assertTrue($writer->has_any_data()); + } + + /** + * Test for provider::delete_data_for_all_users_in_context(). + */ + public function test_delete_data_for_all_users_in_context() { + global $DB; + + // Test setup. + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + // Add two repository_onedrive_access records for the User. + $access = (object)[ + 'usermodified' => $user->id, + 'itemid' => 'Some onedrive access item data', + 'permissionid' => 'Some onedrive access permission data', + 'timecreated' => date('u'), + 'timemodified' => date('u'), + ]; + $DB->insert_record('repository_onedrive_access', $access); + $access = (object)[ + 'usermodified' => $user->id, + 'itemid' => 'Another onedrive access item data', + 'permissionid' => 'Another onedrive access permission data', + 'timecreated' => date('u'), + 'timemodified' => date('u'), + ]; + $DB->insert_record('repository_onedrive_access', $access); + + // Test there are two repository_onedrive_access records for the User. + $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user->id]); + $this->assertCount(2, $access); + + // Test the User's retrieved contextlist contains only one context. + $contextlist = provider::get_contexts_for_userid($user->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + $this->assertEquals($user->id, $context->instanceid); + + // Test delete all users content by context. + provider::delete_data_for_all_users_in_context($context); + $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user->id]); + $this->assertCount(0, $access); + } + + /** + * Test for provider::delete_data_for_user(). + */ + public function test_delete_data_for_user() { + global $DB; + + // Test setup. + $user1 = $this->getDataGenerator()->create_user(); + $user2 = $this->getDataGenerator()->create_user(); + $this->setUser($user1); + + // Add 3 repository_onedrive_accesss records for User 1. + $noaccess = 3; + for ($a = 0; $a < $noaccess; $a++) { + $access = (object)[ + 'usermodified' => $user1->id, + 'itemid' => 'Some onedrive access item data - ' . $a, + 'permissionid' => 'Some onedrive access permission data - ' . $a, + 'timecreated' => date('u'), + 'timemodified' => date('u'), + ]; + $DB->insert_record('repository_onedrive_access', $access); + } + // Add 1 repository_onedrive_accesss record for User 2. + $access = (object)[ + 'usermodified' => $user2->id, + 'itemid' => 'Some onedrive access item data', + 'permissionid' => 'Some onedrive access permission data', + 'timecreated' => date('u'), + 'timemodified' => date('u'), + ]; + $DB->insert_record('repository_onedrive_access', $access); + + // Test the created repository_onedrive records for User 1 equals test number of access specified. + $communities = $DB->get_records('repository_onedrive_access', ['usermodified' => $user1->id]); + $this->assertCount($noaccess, $communities); + + // Test the created repository_onedrive records for User 2 equals 1. + $communities = $DB->get_records('repository_onedrive_access', ['usermodified' => $user2->id]); + $this->assertCount(1, $communities); + + // Test the deletion of repository_onedrive_access records for User 1 results in zero records. + $contextlist = provider::get_contexts_for_userid($user1->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + $this->assertEquals($user1->id, $context->instanceid); + + $approvedcontextlist = new approved_contextlist($user1, 'repository_onedrive', $contextlist->get_contextids()); + provider::delete_data_for_user($approvedcontextlist); + $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user1->id]); + $this->assertCount(0, $access); + + // Test that User 2's single repository_onedrive_access record still exists. + $contextlist = provider::get_contexts_for_userid($user2->id); + $contexts = $contextlist->get_contexts(); + $this->assertCount(1, $contexts); + + // Test the User's contexts equal the User's own context. + $context = reset($contexts); + $this->assertEquals(CONTEXT_USER, $context->contextlevel); + $this->assertEquals($user2->id, $context->instanceid); + $access = $DB->get_records('repository_onedrive_access', ['usermodified' => $user2->id]); + $this->assertCount(1, $access); + } +}