Passed
Push — master ( 7e81b0...7eb007 )
by Robbie
12:39 queued 11s
created

src/Extension/MemberExtension.php (8 issues)

1
<?php declare(strict_types=1);
2
3
namespace SilverStripe\MFA\Extension;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Forms\FieldList;
7
use SilverStripe\MFA\Authenticator\ChangePasswordHandler;
8
use SilverStripe\MFA\Exception\InvalidMethodException;
9
use SilverStripe\MFA\FormField\RegisteredMFAMethodListField;
10
use SilverStripe\MFA\Method\MethodInterface;
11
use SilverStripe\MFA\Model\RegisteredMethod;
12
use SilverStripe\ORM\DataExtension;
13
use SilverStripe\ORM\HasManyList;
14
use SilverStripe\Security\Member;
15
use SilverStripe\Security\Permission;
16
use SilverStripe\Security\PermissionProvider;
17
use SilverStripe\Security\Security;
18
19
/**
20
 * Extend Member to add relationship to registered methods and track some specific preferences
21
 *
22
 * @method RegisteredMethod[]|HasManyList RegisteredMFAMethods
23
 * @property MethodInterface DefaultRegisteredMethod
24
 * @property string DefaultRegisteredMethodID
25
 * @property bool HasSkippedMFARegistration
26
 * @property Member|MemberExtension owner
27
 */
28
class MemberExtension extends DataExtension implements PermissionProvider
29
{
30
    const MFA_ADMINISTER_REGISTERED_METHODS = 'MFA_ADMINISTER_REGISTERED_METHODS';
31
32
    private static $has_many = [
0 ignored issues
show
The private property $has_many is not used, and could be removed.
Loading history...
33
        'RegisteredMFAMethods' => RegisteredMethod::class,
34
    ];
35
36
    private static $db = [
0 ignored issues
show
The private property $db is not used, and could be removed.
Loading history...
37
        'DefaultRegisteredMethodID' => 'Int',
38
        'HasSkippedMFARegistration' => 'Boolean',
39
    ];
40
41
    /**
42
     * Accessor for the `DefaultRegisteredMethod` property.
43
     *
44
     * This is replicating the usual functionality of a has_one relation but does it like this so we can ensure the same
45
     * instance of the MethodInterface is provided regardless if you access it through the has_one or the has_many.
46
     *
47
     * @return RegisteredMethod|null
48
     */
49
    public function getDefaultRegisteredMethod(): ?RegisteredMethod
50
    {
51
        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...
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

51
        return $this->owner->RegisteredMFAMethods()->byId(/** @scrutinizer ignore-type */ $this->owner->DefaultRegisteredMethodID);
Loading history...
52
    }
53
54
    /**
55
     * Set the default registered method for the current member. Does not write the owner record.
56
     *
57
     * @param RegisteredMethod $registeredMethod
58
     * @return Member
59
     * @throws InvalidMethodException
60
     */
61
    public function setDefaultRegisteredMethod(RegisteredMethod $registeredMethod): Member
62
    {
63
        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...
64
            throw new InvalidMethodException('The provided method does not belong to this member');
65
        }
66
        $this->owner->DefaultRegisteredMethodID = $registeredMethod->ID;
67
        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...
68
    }
69
70
    public function updateCMSFields(FieldList $fields): FieldList
71
    {
72
        $fields->removeByName(['DefaultRegisteredMethodID', 'HasSkippedMFARegistration', 'RegisteredMFAMethods']);
73
74
        if (!$this->owner->exists() || !$this->currentUserCanViewMFAConfig()) {
0 ignored issues
show
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

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