Source of file AuditHook.php
Size: 15,290 Bytes - Last Modified: 2021-12-23T10:28:01+00:00
/var/www/docs.ssmods.com/process/src/code/AuditHook.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 | <?php namespace SilverStripe\Auditor; use SilverStripe\Control\Email\Email; use SilverStripe\Core\Injector\Injector; use SilverStripe\ORM\Connect\Database; use SilverStripe\ORM\DataExtension; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectSchema; use SilverStripe\ORM\DB; use SilverStripe\Security\Group; use SilverStripe\Security\Member; use SilverStripe\Security\PermissionRole; use SilverStripe\Security\PermissionRoleCode; use SilverStripe\Security\Security; /** * Provides logging hooks that are inserted into Framework objects. */ class AuditHook extends DataExtension { protected function getAuditLogger() { // We cannot use the 'dependencies' private property, because this will prevent us // from injecting a mock logger for testing. This is because by the time the testing framework // is instantiated, the part of the object graph where AuditLogger lives has already been created. // In other words, Framework does not permit hooking in early enough to adjust the graph when // 'dependencies' is used :-( return Injector::inst()->get('AuditLogger'); } /** * This will bind a new class dynamically so we can hook into manipulation * and capture it. It creates a new PHP file in the temp folder, then * loads it and sets it as the active DB class. * * @deprecated 2.1...3.0 Please use ProxyDBExtension with the tractorcow/silverstripe-proxy-db module instead */ public static function bind_manipulation_capture() { $current = DB::get_conn(); if (!$current || !$current->getConnector()->getSelectedDatabase() || @$current->isManipulationLoggingCapture) { return; } // If not yet set, or its already captured, just return $type = get_class($current); $sanitisedType = str_replace('\\', '_', $type); $file = TEMP_FOLDER . "/.cache.CLC.$sanitisedType"; $dbClass = 'AuditLoggerManipulateCapture_' . $sanitisedType; if (!is_file($file)) { file_put_contents($file, "<?php class $dbClass extends $type { public \$isManipulationLoggingCapture = true; public function manipulate(\$manipulation) { \SilverStripe\Auditor\AuditHook::handle_manipulation(\$manipulation); return parent::manipulate(\$manipulation); } } "); } require_once $file; /** @var Database $captured */ $captured = new $dbClass(); $captured->setConnector($current->getConnector()); $captured->setQueryBuilder($current->getQueryBuilder()); $captured->setSchemaManager($current->getSchemaManager()); // The connection might have had it's name changed (like if we're currently in a test) $captured->selectDatabase($current->getConnector()->getSelectedDatabase()); DB::set_conn($captured); } public static function handle_manipulation($manipulation) { $auditLogger = Injector::inst()->get('AuditLogger'); $currentMember = Security::getCurrentUser(); if (!($currentMember && $currentMember->exists())) { return false; } /** @var DataObjectSchema $schema */ $schema = DataObject::getSchema(); // The tables that we watch for manipulation on $watchedTables = [ $schema->tableName(Member::class), $schema->tableName(Group::class), $schema->tableName(PermissionRole::class), $schema->tableName(PermissionRoleCode::class), ]; foreach ($manipulation as $table => $details) { if (!in_array($details['command'], ['update', 'insert'])) { continue; } // logging writes to specific tables (just not when logging in, as it's noise) if (in_array($table, $watchedTables) && !preg_match('/Security/', @$_SERVER['REQUEST_URI']) && isset($details['id']) ) { $className = $schema->tableClass($table); $data = $className::get()->byID($details['id']); if (!$data) { continue; } $actionText = 'modified '.$table; $extendedText = ''; if ($table === $schema->tableName(Group::class)) { $extendedText = sprintf( 'Effective permissions: %s', implode(', ', $data->Permissions()->column('Code')) ); } if ($table === $schema->tableName(PermissionRole::class)) { $extendedText = sprintf( 'Effective groups: %s, Effective permissions: %s', implode(', ', $data->Groups()->column('Title')), implode(', ', $data->Codes()->column('Code')) ); } if ($table === $schema->tableName(PermissionRoleCode::class)) { $extendedText = sprintf( 'Effective code: %s', $data->Code ); } if ($table === $schema->tableName(Member::class)) { $extendedText = sprintf( 'Effective groups: %s', implode(', ', $data->Groups()->column('Title')) ); } $auditLogger->info(sprintf( '"%s" (ID: %s) %s (ID: %s, ClassName: %s, Title: "%s", %s)', $currentMember->Email ?: $currentMember->Title, $currentMember->ID, $actionText, $details['id'], $data->ClassName, $data->Title, $extendedText )); } // log PermissionRole being added to a Group if ($table === $schema->tableName(Group::class) . '_Roles') { $role = PermissionRole::get()->byId($details['fields']['PermissionRoleID']); $group = Group::get()->byId($details['fields']['GroupID']); // if the permission role isn't already applied to the group if (!DB::query(sprintf( 'SELECT "ID" FROM "Group_Roles" WHERE "GroupID" = %s AND "PermissionRoleID" = %s', $details['fields']['GroupID'], $details['fields']['PermissionRoleID'] ))->value()) { $auditLogger->info(sprintf( '"%s" (ID: %s) added PermissionRole "%s" (ID: %s) to Group "%s" (ID: %s)', $currentMember->Email ?: $currentMember->Title, $currentMember->ID, $role->Title, $role->ID, $group->Title, $group->ID )); } } // log Member added to a Group if ($table === $schema->tableName(Group::class) . '_Members') { $member = Member::get()->byId($details['fields']['MemberID']); $group = Group::get()->byId($details['fields']['GroupID']); // if the user isn't already in the group, log they've been added if (!DB::query(sprintf( 'SELECT "ID" FROM "Group_Members" WHERE "GroupID" = %s AND "MemberID" = %s', $details['fields']['GroupID'], $details['fields']['MemberID'] ))->value()) { $auditLogger->info(sprintf( '"%s" (ID: %s) added Member "%s" (ID: %s) to Group "%s" (ID: %s)', $currentMember->Email ?: $currentMember->Title, $currentMember->ID, $member->Email ?: $member->Title, $member->ID, $group->Title, $group->ID )); } } } } /** * Log a record being published. */ public function onAfterPublish(&$original) { $member = Security::getCurrentUser(); if (!$member || !$member->exists()) { return false; } $effectiveViewerGroups = ''; if ($this->owner->CanViewType === 'OnlyTheseUsers') { $originalViewerGroups = $original ? $original->ViewerGroups()->column('Title') : []; $effectiveViewerGroups = implode(', ', $originalViewerGroups); } if (!$effectiveViewerGroups) { $effectiveViewerGroups = $this->owner->CanViewType; } $effectiveEditorGroups = ''; if ($this->owner->CanEditType === 'OnlyTheseUsers') { $originalEditorGroups = $original ? $original->EditorGroups()->column('Title') : []; $effectiveEditorGroups = implode(', ', $originalEditorGroups); } if (!$effectiveEditorGroups) { $effectiveEditorGroups = $this->owner->CanEditType; } $this->getAuditLogger()->info(sprintf( '"%s" (ID: %s) published %s "%s" (ID: %s, Version: %s, ClassName: %s, Effective ViewerGroups: %s, ' . 'Effective EditorGroups: %s)', $member->Email ?: $member->Title, $member->ID, $this->owner->singular_name(), $this->owner->Title, $this->owner->ID, $this->owner->Version, $this->owner->ClassName, $effectiveViewerGroups, $effectiveEditorGroups )); } /** * Log a record being unpublished. */ public function onAfterUnpublish() { $member = Security::getCurrentUser(); if (!$member || !$member->exists()) { return false; } $this->getAuditLogger()->info(sprintf( '"%s" (ID: %s) unpublished %s "%s" (ID: %s)', $member->Email ?: $member->Title, $member->ID, $this->owner->singular_name(), $this->owner->Title, $this->owner->ID )); } /** * Log a record being reverted to live. */ public function onAfterRevertToLive() { $member = Security::getCurrentUser(); if (!$member || !$member->exists()) { return false; } $this->getAuditLogger()->info(sprintf( '"%s" (ID: %s) reverted %s "%s" (ID: %s) to it\'s live version (#%d)', $member->Email ?: $member->Title, $member->ID, $this->owner->singular_name(), $this->owner->Title, $this->owner->ID, $this->owner->Version )); } /** * Log a record being duplicated. */ public function onAfterDuplicate() { $member = Security::getCurrentUser(); if (!$member || !$member->exists()) { return false; } $this->getAuditLogger()->info(sprintf( '"%s" (ID: %s) duplicated %s "%s" (ID: %s)', $member->Email ?: $member->Title, $member->ID, $this->owner->singular_name(), $this->owner->Title, $this->owner->ID )); } /** * Log a record being deleted. */ public function onAfterDelete() { $member = Security::getCurrentUser(); if (!$member || !$member->exists()) { return false; } $this->getAuditLogger()->info(sprintf( '"%s" (ID: %s) deleted %s "%s" (ID: %s)', $member->Email ?: $member->Title, $member->ID, $this->owner->singular_name(), $this->owner->Title, $this->owner->ID )); } /** * Log a record being restored to stage. */ public function onAfterRestoreToStage() { $member = Security::getCurrentUser(); if (!$member || !$member->exists()) { return false; } $this->getAuditLogger()->info(sprintf( '"%s" (ID: %s) restored %s "%s" to stage (ID: %s)', $member->Email ?: $member->Title, $member->ID, $this->owner->singular_name(), $this->owner->Title, $this->owner->ID )); } /** * Log successful login attempts. */ public function afterMemberLoggedIn() { $this->getAuditLogger()->info(sprintf( '"%s" (ID: %s) successfully logged in', $this->owner->Email ?: $this->owner->Title, $this->owner->ID )); } /** * Log successfully restored sessions from "remember me" cookies ("auto login"). */ public function memberAutoLoggedIn() { $this->getAuditLogger()->info(sprintf( '"%s" (ID: %s) successfully restored autologin session', $this->owner->Email ?: $this->owner->Title, $this->owner->ID )); } /** * Log failed login attempts. */ public function authenticationFailed($data) { // LDAP authentication uses a "Login" POST field instead of Email. $login = isset($data['Login']) ? $data['Login'] : (isset($data[Email::class]) ? $data[Email::class] : ''); if (empty($login)) { return $this->getAuditLogger()->warning( 'Could not determine username/email of failed authentication. '. 'This could be due to login form not using Email or Login field for POST data.' ); } $this->getAuditLogger()->info(sprintf('Failed login attempt using email "%s"', $login)); } /** * @deprecated 2.1...3.0 Use tractorcow/silverstripe-proxy-db instead */ public function onBeforeInit() { // no-op } /** * Log permission failures (where the status is set after init of page). */ public function onAfterInit() { // Suppress errors if dev/build necessary if (!Security::database_is_ready()) { return false; } $currentMember = Security::getCurrentUser(); if (!$currentMember || !$currentMember->exists()) { return false; } $statusCode = $this->owner->getResponse()->getStatusCode(); if (substr($statusCode, 0, 1) == '4') { $this->logPermissionDenied($statusCode, $currentMember); } } protected function logPermissionDenied($statusCode, $member) { $this->getAuditLogger()->info(sprintf( 'HTTP code %s - "%s" (ID: %s) is denied access to %s', $statusCode, $member->Email ?: $member->Title, $member->ID, $_SERVER['REQUEST_URI'] )); } /** * Log successful logout. */ public function afterMemberLoggedOut() { $this->getAuditLogger()->info(sprintf( '"%s" (ID: %s) successfully logged out', $this->owner->Email ?: $this->owner->Title, $this->owner->ID )); } } |