MemberExtension::afterMemberLoggedIn()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 6
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 10
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SilverStripe\MFA\Extension;
6
7
use SilverStripe\Control\Controller;
8
use SilverStripe\Forms\FieldList;
9
use SilverStripe\MFA\Authenticator\ChangePasswordHandler;
10
use SilverStripe\MFA\Exception\InvalidMethodException;
11
use SilverStripe\MFA\FormField\RegisteredMFAMethodListField;
12
use SilverStripe\MFA\Method\MethodInterface;
13
use SilverStripe\MFA\Model\RegisteredMethod;
14
use SilverStripe\ORM\DataExtension;
15
use SilverStripe\ORM\HasManyList;
16
use SilverStripe\Security\Member;
17
use SilverStripe\Security\Permission;
18
use SilverStripe\Security\PermissionProvider;
19
use SilverStripe\Security\Security;
20
21
/**
22
 * Extend Member to add relationship to registered methods and track some specific preferences
23
 *
24
 * @method RegisteredMethod[]|HasManyList RegisteredMFAMethods
25
 * @property MethodInterface DefaultRegisteredMethod
26
 * @property string DefaultRegisteredMethodID
27
 * @property bool HasSkippedMFARegistration
28
 * @property Member|MemberExtension owner
29
 */
30
class MemberExtension extends DataExtension implements PermissionProvider
31
{
32
    public const MFA_ADMINISTER_REGISTERED_METHODS = 'MFA_ADMINISTER_REGISTERED_METHODS';
33
34
    private static $has_many = [
0 ignored issues
show
introduced by
The private property $has_many is not used, and could be removed.
Loading history...
35
        'RegisteredMFAMethods' => RegisteredMethod::class,
36
    ];
37
38
    private static $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
39
        'DefaultRegisteredMethodID' => 'Int',
40
        'HasSkippedMFARegistration' => 'Boolean',
41
    ];
42
43
    /**
44
     * Accessor for the `DefaultRegisteredMethod` property.
45
     *
46
     * This is replicating the usual functionality of a has_one relation but does it like this so we can ensure the same
47
     * instance of the MethodInterface is provided regardless if you access it through the has_one or the has_many.
48
     *
49
     * @return RegisteredMethod|null
50
     */
51
    public function getDefaultRegisteredMethod(): ?RegisteredMethod
52
    {
53
        return $this->owner->RegisteredMFAMethods()->byId($this->owner->DefaultRegisteredMethodID);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->owner->Reg...aultRegisteredMethodID) returns the type SilverStripe\ORM\DataObject which includes types incompatible with the type-hinted return SilverStripe\MFA\Model\RegisteredMethod|null.
Loading history...
Bug introduced by
It seems like $this->owner->DefaultRegisteredMethodID can also be of type string; however, parameter $id of SilverStripe\ORM\DataList::byID() does only seem to accept integer, 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

53
        return $this->owner->RegisteredMFAMethods()->byId(/** @scrutinizer ignore-type */ $this->owner->DefaultRegisteredMethodID);
Loading history...
54
    }
55
56
    /**
57
     * Set the default registered method for the current member. Does not write the owner record.
58
     *
59
     * @param RegisteredMethod $registeredMethod
60
     * @return Member
61
     * @throws InvalidMethodException
62
     */
63
    public function setDefaultRegisteredMethod(RegisteredMethod $registeredMethod): Member
64
    {
65
        if ($registeredMethod->Member()->ID != $this->owner->ID) {
0 ignored issues
show
Bug Best Practice introduced by
The property ID does not exist on SilverStripe\MFA\Extension\MemberExtension. Did you maybe forget to declare it?
Loading history...
66
            throw new InvalidMethodException('The provided method does not belong to this member');
67
        }
68
        $this->owner->DefaultRegisteredMethodID = $registeredMethod->ID;
69
        return $this->owner;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->owner could return the type SilverStripe\MFA\Extension\MemberExtension which is incompatible with the type-hinted return SilverStripe\Security\Member. Consider adding an additional type-check to rule them out.
Loading history...
70
    }
71
72
    public function updateCMSFields(FieldList $fields): FieldList
73
    {
74
        $fields->removeByName(['DefaultRegisteredMethodID', 'HasSkippedMFARegistration', 'RegisteredMFAMethods']);
75
76
        if (!$this->owner->exists() || !$this->currentUserCanViewMFAConfig()) {
0 ignored issues
show
Bug introduced by
The method exists() does not exist on SilverStripe\MFA\Extension\MemberExtension. ( Ignorable by Annotation )

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

76
        if (!$this->owner->/** @scrutinizer ignore-call */ exists() || !$this->currentUserCanViewMFAConfig()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
77
            return $fields;
78
        }
79
80
        $fields->addFieldToTab(
81
            'Root.Main',
82
            $methodListField = RegisteredMFAMethodListField::create(
83
                'MFASettings',
84
                _t(__CLASS__ . '.MFA_SETTINGS_FIELD_LABEL', 'Multi-factor authentication settings (MFA)'),
85
                $this->owner->ID
0 ignored issues
show
Bug Best Practice introduced by
The property ID does not exist on SilverStripe\MFA\Extension\MemberExtension. Did you maybe forget to declare it?
Loading history...
86
            )
87
        );
88
89
        if (!$this->currentUserCanEditMFAConfig()) {
90
            $methodListField->setReadonly(true);
91
        }
92
93
        return $fields;
94
    }
95
96
    /**
97
     * Determines whether the logged in user has sufficient permission to see the MFA config for this Member.
98
     *
99
     * @return bool
100
     */
101
    public function currentUserCanViewMFAConfig(): bool
102
    {
103
        return (Permission::check(self::MFA_ADMINISTER_REGISTERED_METHODS)
104
            || $this->currentUserCanEditMFAConfig());
105
    }
106
107
    /**
108
     * Determines whether the logged in user has sufficient permission to modify the MFA config for this Member.
109
     * Note that this is different from being able to _reset_ the config (which administrators can do).
110
     *
111
     * @return bool
112
     */
113
    public function currentUserCanEditMFAConfig(): bool
114
    {
115
        return (Security::getCurrentUser() && Security::getCurrentUser()->ID === $this->owner->ID);
0 ignored issues
show
Bug Best Practice introduced by
The property ID does not exist on SilverStripe\MFA\Extension\MemberExtension. Did you maybe forget to declare it?
Loading history...
116
    }
117
118
    /**
119
     * Provides the MFA view/reset permission for selection in the permission list in the CMS.
120
     *
121
     * @return array
122
     */
123
    public function providePermissions(): array
124
    {
125
        $label = _t(
126
            __CLASS__ . '.MFA_PERMISSION_LABEL',
127
            'View/reset MFA configuration for other members'
128
        );
129
130
        $category = _t(
131
            'SilverStripe\\Security\\Permission.PERMISSIONS_CATEGORY',
132
            'Roles and access permissions'
133
        );
134
135
        $description = _t(
136
            __CLASS__ . '.MFA_PERMISSION_DESCRIPTION',
137
            'Ability to view and reset registered MFA methods for other members.'
138
            . ' Requires the "Access to \'Security\' section" permission.'
139
        );
140
141
        return [
142
            self::MFA_ADMINISTER_REGISTERED_METHODS => [
143
                'name' => $label,
144
                'category' => $category,
145
                'help' => $description,
146
                'sort' => 200,
147
            ],
148
        ];
149
    }
150
151
    /**
152
     * Clear any temporary multi-factor authentication related session keys when a member is successfully logged in.
153
     */
154
    public function afterMemberLoggedIn(): void
155
    {
156
        if (!Controller::has_curr()) {
157
            return;
158
        }
159
160
        Controller::curr()
161
            ->getRequest()
162
            ->getSession()
163
            ->clear(ChangePasswordHandler::MFA_VERIFIED_ON_CHANGE_PASSWORD);
164
    }
165
}
166