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 = [ |
||
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; |
||
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); |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||
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 |