lekoala /
silverstripe-admini
| 1 | <?php |
||
| 2 | |||
| 3 | namespace LeKoala\Admini; |
||
| 4 | |||
| 5 | use SilverStripe\ORM\DB; |
||
| 6 | use SilverStripe\Forms\Form; |
||
| 7 | use SilverStripe\Forms\TabSet; |
||
| 8 | use SilverStripe\ORM\ArrayList; |
||
| 9 | use LeKoala\Base\View\Bootstrap; |
||
| 10 | use SilverStripe\Security\Group; |
||
| 11 | use SilverStripe\View\ArrayData; |
||
| 12 | use SilverStripe\Forms\FieldList; |
||
| 13 | use SilverStripe\Security\Member; |
||
| 14 | use SilverStripe\Control\Director; |
||
| 15 | use SilverStripe\Forms\HeaderField; |
||
| 16 | use SilverStripe\Security\Security; |
||
| 17 | use LeKoala\Tabulator\TabulatorGrid; |
||
| 18 | use SilverStripe\Forms\LiteralField; |
||
| 19 | use SilverStripe\Control\HTTPRequest; |
||
| 20 | use SilverStripe\Security\Permission; |
||
| 21 | use LeKoala\Admini\Helpers\FileHelper; |
||
| 22 | use SilverStripe\Security\LoginAttempt; |
||
| 23 | use SilverStripe\Security\PermissionRole; |
||
| 24 | use LeKoala\CmsActions\CmsInlineFormAction; |
||
| 25 | use LeKoala\Admini\Forms\BootstrapAlertField; |
||
| 26 | use SilverStripe\Security\PermissionProvider; |
||
| 27 | |||
| 28 | /** |
||
| 29 | * Security section of the CMS |
||
| 30 | */ |
||
| 31 | class SecurityAdmin extends ModelAdmin implements PermissionProvider |
||
| 32 | { |
||
| 33 | private static $url_segment = 'security'; |
||
| 34 | |||
| 35 | private static $menu_title = 'Security'; |
||
| 36 | |||
| 37 | private static $managed_models = [ |
||
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
| 38 | 'users' => [ |
||
| 39 | 'title' => 'Users', |
||
| 40 | 'dataClass' => Member::class |
||
| 41 | ], |
||
| 42 | 'groups' => [ |
||
| 43 | 'title' => 'Groups', |
||
| 44 | 'dataClass' => Group::class |
||
| 45 | ], |
||
| 46 | // 'roles' => [ |
||
| 47 | // 'title' => 'Roles', |
||
| 48 | // 'dataClass' => PermissionRole::class |
||
| 49 | // ], |
||
| 50 | ]; |
||
| 51 | |||
| 52 | private static $required_permission_codes = 'CMS_ACCESS_SecurityAdmin'; |
||
| 53 | |||
| 54 | private static $menu_icon = MaterialIcons::SECURITY; |
||
|
0 ignored issues
–
show
|
|||
| 55 | |||
| 56 | private static $allowed_actions = [ |
||
| 57 | 'doClearLogs', |
||
| 58 | 'doRotateLogs', |
||
| 59 | // new tabs |
||
| 60 | 'security_audit', |
||
| 61 | 'logs', |
||
| 62 | ]; |
||
| 63 | |||
| 64 | public static function getMembersFromSecurityGroupsIDs(): array |
||
| 65 | { |
||
| 66 | $sql = 'SELECT DISTINCT MemberID FROM Group_Members INNER JOIN Permission ON Permission.GroupID = Group_Members.GroupID WHERE Code LIKE \'CMS_%\' OR Code = \'ADMIN\''; |
||
| 67 | return DB::query($sql)->column(); |
||
| 68 | } |
||
| 69 | |||
| 70 | /** |
||
| 71 | * @param array $extraIDs |
||
| 72 | * @return Member[]|ArrayList |
||
| 73 | */ |
||
| 74 | public static function getMembersFromSecurityGroups(array $extraIDs = []) |
||
| 75 | { |
||
| 76 | $ids = array_merge(self::getMembersFromSecurityGroupsIDs(), $extraIDs); |
||
| 77 | return Member::get()->filter('ID', $ids); |
||
| 78 | } |
||
| 79 | |||
| 80 | public function getManagedModels() |
||
| 81 | { |
||
| 82 | $models = parent::getManagedModels(); |
||
| 83 | |||
| 84 | // Add extra tabs |
||
| 85 | if (Security::config()->login_recording) { |
||
| 86 | $models['security_audit'] = [ |
||
| 87 | 'title' => 'Security Audit', |
||
| 88 | 'dataClass' => LoginAttempt::class, |
||
| 89 | ]; |
||
| 90 | } |
||
| 91 | if (Permission::check('ADMIN')) { |
||
| 92 | $models['logs'] = [ |
||
| 93 | 'title' => 'Logs', |
||
| 94 | 'dataClass' => LoginAttempt::class, // mock class |
||
| 95 | ]; |
||
| 96 | } |
||
| 97 | |||
| 98 | return $models; |
||
| 99 | } |
||
| 100 | |||
| 101 | public function getEditForm($id = null, $fields = null) |
||
| 102 | { |
||
| 103 | $form = parent::getEditForm($id, $fields); |
||
| 104 | |||
| 105 | // In security, we only show group members + current item (to avoid issue when creating stuff) |
||
| 106 | $request = $this->getRequest(); |
||
| 107 | $dirParts = explode('/', $request->remaining()); |
||
| 108 | $currentID = isset($dirParts[3]) ? [$dirParts[3]] : []; |
||
| 109 | |||
| 110 | switch ($this->modelTab) { |
||
| 111 | case 'users': |
||
| 112 | /** @var TabulatorGrid $members */ |
||
| 113 | $members = $form->Fields()->dataFieldByName('users'); |
||
| 114 | $membersOfGroups = self::getMembersFromSecurityGroups($currentID); |
||
| 115 | $members->setList($membersOfGroups); |
||
| 116 | |||
| 117 | // Add some security wise fields |
||
| 118 | $sng = singleton($members->getModelClass()); |
||
| 119 | if ($sng->hasMethod('DirectGroupsList')) { |
||
| 120 | $members->addDisplayFields([ |
||
| 121 | 'DirectGroupsList' => 'Direct Groups' |
||
| 122 | ]); |
||
| 123 | } |
||
| 124 | if ($sng->hasMethod('Is2FaConfigured')) { |
||
| 125 | $members->addDisplayFields([ |
||
| 126 | 'Is2FaConfigured' => '2FA' |
||
| 127 | ]); |
||
| 128 | } |
||
| 129 | break; |
||
| 130 | case 'groups': |
||
| 131 | break; |
||
| 132 | case 'security_audit': |
||
| 133 | if (Security::config()->login_recording) { |
||
| 134 | $this->addAuditTab($form); |
||
| 135 | } |
||
| 136 | break; |
||
| 137 | case 'logs': |
||
| 138 | if (Permission::check('ADMIN')) { |
||
| 139 | $this->addLogTab($form); |
||
| 140 | } |
||
| 141 | break; |
||
| 142 | } |
||
| 143 | |||
| 144 | return $form; |
||
| 145 | } |
||
| 146 | |||
| 147 | protected function getLogFiles(): array |
||
| 148 | { |
||
| 149 | $logDir = Director::baseFolder(); |
||
| 150 | $logFiles = glob($logDir . '/*.log'); |
||
| 151 | return $logFiles; |
||
| 152 | } |
||
| 153 | |||
| 154 | protected function addLogTab(Form $form) |
||
| 155 | { |
||
| 156 | $logFiles = $this->getLogFiles(); |
||
| 157 | $logTab = $form->Fields(); |
||
| 158 | $logTab->removeByName('logs'); |
||
| 159 | |||
| 160 | foreach ($logFiles as $logFile) { |
||
| 161 | $logName = pathinfo($logFile, PATHINFO_FILENAME); |
||
| 162 | |||
| 163 | $logTab->push(new HeaderField($logName, ucwords($logName))); |
||
| 164 | |||
| 165 | $filemtime = filemtime($logFile); |
||
| 166 | $filesize = filesize($logFile); |
||
| 167 | |||
| 168 | $logTab->push(new BootstrapAlertField($logName . 'Alert', _t('SecurityAdmin.LogAlert', "Last updated on {updated}", [ |
||
| 169 | 'updated' => date('Y-m-d H:i:s', $filemtime), |
||
| 170 | ]))); |
||
| 171 | |||
| 172 | $lastLines = '<pre>' . FileHelper::tail($logFile, 10) . '</pre>'; |
||
| 173 | |||
| 174 | $logTab->push(new LiteralField($logName, $lastLines)); |
||
| 175 | $logTab->push(new LiteralField($logName . 'Size', '<p>' . _t('SecurityAdmin.LogSize', "Total size is {size}", [ |
||
| 176 | 'size' => FileHelper::humanFilesize($filesize) |
||
| 177 | ]) . '</p>')); |
||
| 178 | } |
||
| 179 | |||
| 180 | $clearLogsBtn = new CmsInlineFormAction('doClearLogs', _t('SecurityAdmin.doClearLogs', 'Clear Logs')); |
||
| 181 | $logTab->push($clearLogsBtn); |
||
| 182 | $rotateLogsBtn = new CmsInlineFormAction('doRotateLogs', _t('SecurityAdmin.doRotateLogs', 'Rotate Logs')); |
||
| 183 | $logTab->push($rotateLogsBtn); |
||
| 184 | } |
||
| 185 | |||
| 186 | public function doClearLogs(HTTPRequest $request) |
||
| 187 | { |
||
| 188 | foreach ($this->getLogFiles() as $logFile) { |
||
| 189 | unlink($logFile); |
||
| 190 | } |
||
| 191 | $msg = "Logs cleared"; |
||
| 192 | return $this->redirectWithStatus($msg); |
||
| 193 | } |
||
| 194 | |||
| 195 | public function doRotateLogs(HTTPRequest $request) |
||
| 196 | { |
||
| 197 | foreach ($this->getLogFiles() as $logFile) { |
||
| 198 | if (strpos($logFile, '-') !== false) { |
||
| 199 | continue; |
||
| 200 | } |
||
| 201 | $newname = dirname($logFile) . '/' . pathinfo($logFile, PATHINFO_FILENAME) . '-' . date('Ymd') . '.log'; |
||
| 202 | rename($logFile, $newname); |
||
| 203 | } |
||
| 204 | $msg = "Logs rotated"; |
||
| 205 | return $this->redirectWithStatus($msg); |
||
| 206 | } |
||
| 207 | |||
| 208 | protected function addAuditTab(Form $form) |
||
| 209 | { |
||
| 210 | $auditTab = $form->Fields(); |
||
| 211 | $auditTab->removeByName('security_audit'); |
||
| 212 | |||
| 213 | $Member_SNG = Member::singleton(); |
||
| 214 | $membersLocked = Member::get()->where('LockedOutUntil > NOW()'); |
||
| 215 | if ($membersLocked->count()) { |
||
| 216 | $membersLockedGrid = new TabulatorGrid('MembersLocked', _t('SecurityAdmin.LockedMembers', "Locked Members"), $membersLocked); |
||
| 217 | $membersLockedGrid->setForm($form); |
||
| 218 | $membersLockedGrid->setDisplayFields([ |
||
| 219 | 'Title' => $Member_SNG->fieldLabel('Title'), |
||
| 220 | 'Email' => $Member_SNG->fieldLabel('Email'), |
||
| 221 | 'LockedOutUntil' => $Member_SNG->fieldLabel('LockedOutUntil'), |
||
| 222 | 'FailedLoginCount' => $Member_SNG->fieldLabel('FailedLoginCount'), |
||
| 223 | ]); |
||
| 224 | $auditTab->push($membersLockedGrid); |
||
| 225 | } |
||
| 226 | |||
| 227 | $LoginAttempt_SNG = LoginAttempt::singleton(); |
||
| 228 | |||
| 229 | $getMembersFromSecurityGroupsIDs = self::getMembersFromSecurityGroupsIDs(); |
||
| 230 | $recentAdminLogins = LoginAttempt::get()->filter([ |
||
| 231 | 'Status' => 'Success', |
||
| 232 | 'MemberID' => $getMembersFromSecurityGroupsIDs |
||
| 233 | ])->limit(10)->sort('Created DESC'); |
||
| 234 | $recentAdminLoginsGrid = new TabulatorGrid('RecentAdminLogins', _t('SecurityAdmin.RecentAdminLogins', "Recent Admin Logins"), $recentAdminLogins); |
||
| 235 | $recentAdminLoginsGrid->setDisplayFields([ |
||
| 236 | 'Created' => $LoginAttempt_SNG->fieldLabel('Created'), |
||
| 237 | 'IP' => $LoginAttempt_SNG->fieldLabel('IP'), |
||
| 238 | 'Member.Title' => $Member_SNG->fieldLabel('Title'), |
||
| 239 | 'Member.Email' => $Member_SNG->fieldLabel('Email'), |
||
| 240 | ]); |
||
| 241 | $recentAdminLoginsGrid->setViewOnly(); |
||
| 242 | $recentAdminLoginsGrid->setForm($form); |
||
| 243 | $auditTab->push($recentAdminLoginsGrid); |
||
| 244 | |||
| 245 | $recentPasswordFailures = LoginAttempt::get()->filter('Status', 'Failure')->limit(10)->sort('Created DESC'); |
||
| 246 | $recentPasswordFailuresGrid = new TabulatorGrid('RecentPasswordFailures', _t('SecurityAdmin.RecentPasswordFailures', "Recent Password Failures"), $recentPasswordFailures); |
||
| 247 | $recentPasswordFailuresGrid->setDisplayFields([ |
||
| 248 | 'Created' => $LoginAttempt_SNG->fieldLabel('Created'), |
||
| 249 | 'IP' => $LoginAttempt_SNG->fieldLabel('IP'), |
||
| 250 | 'Member.Title' => $Member_SNG->fieldLabel('Title'), |
||
| 251 | 'Member.Email' => $Member_SNG->fieldLabel('Email'), |
||
| 252 | 'Member.FailedLoginCount' => $Member_SNG->fieldLabel('FailedLoginCount'), |
||
| 253 | ]); |
||
| 254 | $recentPasswordFailuresGrid->setViewOnly(); |
||
| 255 | $recentPasswordFailuresGrid->setForm($form); |
||
| 256 | $auditTab->push($recentPasswordFailuresGrid); |
||
| 257 | } |
||
| 258 | |||
| 259 | public function providePermissions() |
||
| 260 | { |
||
| 261 | $title = $this->menu_title(); |
||
| 262 | return array( |
||
| 263 | "CMS_ACCESS_SecurityAdmin" => [ |
||
| 264 | 'name' => _t( |
||
| 265 | 'LeKoala\\Admini\\LeftAndMain.ACCESS', |
||
| 266 | "Access to '{title}' section", |
||
| 267 | ['title' => $title] |
||
| 268 | ), |
||
| 269 | 'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'), |
||
| 270 | 'help' => _t( |
||
| 271 | __CLASS__ . '.ACCESS_HELP', |
||
| 272 | 'Allow viewing, adding and editing users, as well as assigning permissions and roles to them.' |
||
| 273 | ) |
||
| 274 | ], |
||
| 275 | 'EDIT_PERMISSIONS' => array( |
||
| 276 | 'name' => _t(__CLASS__ . '.EDITPERMISSIONS', 'Manage permissions for groups'), |
||
| 277 | 'category' => _t( |
||
| 278 | 'SilverStripe\\Security\\Permission.PERMISSIONS_CATEGORY', |
||
| 279 | 'Roles and access permissions' |
||
| 280 | ), |
||
| 281 | 'help' => _t( |
||
| 282 | __CLASS__ . '.EDITPERMISSIONS_HELP', |
||
| 283 | 'Ability to edit Permissions and IP Addresses for a group.' |
||
| 284 | . ' Requires the "Access to \'Security\' section" permission.' |
||
| 285 | ), |
||
| 286 | 'sort' => 0 |
||
| 287 | ), |
||
| 288 | 'APPLY_ROLES' => array( |
||
| 289 | 'name' => _t(__CLASS__ . '.APPLY_ROLES', 'Apply roles to groups'), |
||
| 290 | 'category' => _t( |
||
| 291 | 'SilverStripe\\Security\\Permission.PERMISSIONS_CATEGORY', |
||
| 292 | 'Roles and access permissions' |
||
| 293 | ), |
||
| 294 | 'help' => _t( |
||
| 295 | __CLASS__ . '.APPLY_ROLES_HELP', |
||
| 296 | 'Ability to edit the roles assigned to a group.' |
||
| 297 | . ' Requires the "Access to \'Users\' section" permission.' |
||
| 298 | ), |
||
| 299 | 'sort' => 0 |
||
| 300 | ) |
||
| 301 | ); |
||
| 302 | } |
||
| 303 | } |
||
| 304 |