Passed
Push — master ( 06fadb...21c0e7 )
by Thomas
04:14 queued 01:30
created

SecurityAdmin::getEditForm()   B

Complexity

Conditions 10
Paths 20

Size

Total Lines 44
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 28
c 1
b 0
f 0
nc 20
nop 2
dl 0
loc 44
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
0 ignored issues
show
Bug introduced by
The type LeKoala\Base\View\Bootstrap was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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;
0 ignored issues
show
Bug introduced by
The type LeKoala\CmsActions\CmsInlineFormAction was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
The private property $managed_models is not used, and could be removed.
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
introduced by
The private property $menu_icon is not used, and could be removed.
Loading history...
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
The expression return SilverStripe\Secu...t()->filter('ID', $ids) returns the type SilverStripe\ORM\DataList which is incompatible with the documented return type SilverStripe\Security\Me...verStripe\ORM\ArrayList.
Loading history...
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)));
0 ignored issues
show
Bug introduced by
It seems like $logName can also be of type array; however, parameter $name of SilverStripe\Forms\HeaderField::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

163
            $logTab->push(new HeaderField(/** @scrutinizer ignore-type */ $logName, ucwords($logName)));
Loading history...
Bug introduced by
It seems like $logName can also be of type array; however, parameter $string of ucwords() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

163
            $logTab->push(new HeaderField($logName, ucwords(/** @scrutinizer ignore-type */ $logName)));
Loading history...
164
165
            $filemtime = filemtime($logFile);
166
            $filesize = filesize($logFile);
167
168
            $logTab->push(new BootstrapAlertField($logName . 'Alert', _t('BaseSecurityAdminExtension.LogAlert', "Last updated on {updated}", [
0 ignored issues
show
Bug introduced by
Are you sure $logName of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

168
            $logTab->push(new BootstrapAlertField(/** @scrutinizer ignore-type */ $logName . 'Alert', _t('BaseSecurityAdminExtension.LogAlert', "Last updated on {updated}", [
Loading history...
169
                'updated' => date('Y-m-d H:i:s', $filemtime),
170
            ])));
171
172
            $lastLines = '<pre>' . FileHelper::tail($logFile, 10) . '</pre>';
0 ignored issues
show
Bug introduced by
Are you sure LeKoala\Admini\Helpers\F...per::tail($logFile, 10) of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

172
            $lastLines = '<pre>' . /** @scrutinizer ignore-type */ FileHelper::tail($logFile, 10) . '</pre>';
Loading history...
173
174
            $logTab->push(new LiteralField($logName, $lastLines));
0 ignored issues
show
Bug introduced by
It seems like $logName can also be of type array; however, parameter $name of SilverStripe\Forms\LiteralField::__construct() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

174
            $logTab->push(new LiteralField(/** @scrutinizer ignore-type */ $logName, $lastLines));
Loading history...
175
            $logTab->push(new LiteralField($logName . 'Size', '<p>' . _t('BaseSecurityAdminExtension.LogSize', "Total size is {size}", [
176
                'size' => FileHelper::humanFilesize($filesize)
177
            ]) . '</p>'));
178
        }
179
180
        $clearLogsBtn = new CmsInlineFormAction('doClearLogs', _t('BaseSecurityAdminExtension.doClearLogs', 'Clear Logs'));
181
        $logTab->push($clearLogsBtn);
182
        $rotateLogsBtn = new CmsInlineFormAction('doRotateLogs', _t('BaseSecurityAdminExtension.doRotateLogs', 'Rotate Logs'));
183
        $logTab->push($rotateLogsBtn);
184
    }
185
186
    public function doClearLogs(HTTPRequest $request)
0 ignored issues
show
Unused Code introduced by
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 ignore-unused  annotation

186
    public function doClearLogs(/** @scrutinizer ignore-unused */ HTTPRequest $request)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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
Unused Code introduced by
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 ignore-unused  annotation

195
    public function doRotateLogs(/** @scrutinizer ignore-unused */ HTTPRequest $request)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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';
0 ignored issues
show
Bug introduced by
Are you sure pathinfo($logFile, LeKoa...mini\PATHINFO_FILENAME) of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

201
            $newname = dirname($logFile) . '/' . /** @scrutinizer ignore-type */ pathinfo($logFile, PATHINFO_FILENAME) . '-' . date('Ymd') . '.log';
Loading history...
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('BaseSecurityAdminExtension.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('BaseSecurityAdminExtension.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->setForm($form);
242
        $auditTab->push($recentAdminLoginsGrid);
243
244
        $recentPasswordFailures = LoginAttempt::get()->filter('Status', 'Failure')->limit(10)->sort('Created DESC');
245
        $recentPasswordFailuresGrid = new TabulatorGrid('RecentPasswordFailures', _t('BaseSecurityAdminExtension.RecentPasswordFailures', "Recent Password Failures"), $recentPasswordFailures);
246
        $recentPasswordFailuresGrid->setDisplayFields([
247
            'Created' => $LoginAttempt_SNG->fieldLabel('Created'),
248
            'IP' => $LoginAttempt_SNG->fieldLabel('IP'),
249
            'Member.Title' => $Member_SNG->fieldLabel('Title'),
250
            'Member.Email' => $Member_SNG->fieldLabel('Email'),
251
            'Member.FailedLoginCount' => $Member_SNG->fieldLabel('FailedLoginCount'),
252
        ]);
253
        $recentPasswordFailuresGrid->setForm($form);
254
        $auditTab->push($recentPasswordFailuresGrid);
255
    }
256
257
    public function providePermissions()
258
    {
259
        $title = $this->menu_title();
260
        return array(
261
            "CMS_ACCESS_SecurityAdmin" => [
262
                'name' => _t(
263
                    'LeKoala\\Admini\\LeftAndMain.ACCESS',
264
                    "Access to '{title}' section",
265
                    ['title' => $title]
266
                ),
267
                'category' => _t('SilverStripe\\Security\\Permission.CMS_ACCESS_CATEGORY', 'CMS Access'),
268
                'help' => _t(
269
                    __CLASS__ . '.ACCESS_HELP',
270
                    'Allow viewing, adding and editing users, as well as assigning permissions and roles to them.'
271
                )
272
            ],
273
            'EDIT_PERMISSIONS' => array(
274
                'name' => _t(__CLASS__ . '.EDITPERMISSIONS', 'Manage permissions for groups'),
275
                'category' => _t(
276
                    'SilverStripe\\Security\\Permission.PERMISSIONS_CATEGORY',
277
                    'Roles and access permissions'
278
                ),
279
                'help' => _t(
280
                    __CLASS__ . '.EDITPERMISSIONS_HELP',
281
                    'Ability to edit Permissions and IP Addresses for a group.'
282
                        . ' Requires the "Access to \'Security\' section" permission.'
283
                ),
284
                'sort' => 0
285
            ),
286
            'APPLY_ROLES' => array(
287
                'name' => _t(__CLASS__ . '.APPLY_ROLES', 'Apply roles to groups'),
288
                'category' => _t(
289
                    'SilverStripe\\Security\\Permission.PERMISSIONS_CATEGORY',
290
                    'Roles and access permissions'
291
                ),
292
                'help' => _t(
293
                    __CLASS__ . '.APPLY_ROLES_HELP',
294
                    'Ability to edit the roles assigned to a group.'
295
                        . ' Requires the "Access to \'Users\' section" permission.'
296
                ),
297
                'sort' => 0
298
            )
299
        );
300
    }
301
}
302