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); |
||||
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) |
||||
0 ignored issues
–
show
|
|||||
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) |
||||
0 ignored issues
–
show
The parameter
$request is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body. ![]() |
|||||
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 |
This check looks for parameters that have been defined for a function or method, but which are not used in the method body.