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

RegisteredMethodManager::canRemoveMethod()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 17
rs 9.9332
1
<?php declare(strict_types=1);
2
3
namespace SilverStripe\MFA\Service;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\Core\Extensible;
7
use SilverStripe\Core\Injector\Injectable;
8
use SilverStripe\Core\Injector\Injector;
9
use SilverStripe\MFA\Extension\MemberExtension;
10
use SilverStripe\MFA\Method\MethodInterface;
11
use SilverStripe\MFA\Model\RegisteredMethod;
12
use SilverStripe\Security\Member;
13
14
/**
15
 * The RegisteredMethodManager service class facilitates the communication of Members and RegisteredMethod instances
16
 * in a reusable singleton.
17
 */
18
class RegisteredMethodManager
19
{
20
    use Extensible;
21
    use Injectable;
22
23
    private static $dependencies = [
0 ignored issues
show
introduced by
The private property $dependencies is not used, and could be removed.
Loading history...
24
        'NotificationService' => '%$' . Notification::class
25
    ];
26
27
    /**
28
     * @var Notification
29
     */
30
    protected $notification;
31
32
    public function setNotificationService(Notification $notification): self
33
    {
34
        $this->notification = $notification;
35
        return $this;
36
    }
37
38
    /**
39
     * Get an authentication method object matching the given method from the given member. Returns null if the given
40
     * method could not be found attached to the Member
41
     *
42
     * @param Member&MemberExtension $member
43
     * @param MethodInterface $method
44
     * @return RegisteredMethod|null
45
     */
46
    public function getFromMember(Member $member, MethodInterface $method): ?RegisteredMethod
47
    {
48
        // Find the actual method registration data object from the member for the specified default authenticator
49
        foreach ($member->RegisteredMFAMethods() as $registeredMethod) {
50
            if ($registeredMethod->getMethod()->getURLSegment() === $method->getURLSegment()) {
51
                return $registeredMethod;
52
            }
53
        }
54
55
        return null;
56
    }
57
58
    /**
59
     * Fetch an existing RegisteredMethod object from the Member or make a new one, and then ensure it's associated
60
     * to the given Member
61
     *
62
     * @param Member&MemberExtension $member
63
     * @param MethodInterface $method
64
     * @param mixed $data
65
     * @return bool Whether the method was added/replace
66
     * @throws \SilverStripe\ORM\ValidationException
67
     */
68
    public function registerForMember(Member $member, MethodInterface $method, $data = null): bool
69
    {
70
        if (empty($data)) {
71
            return false;
72
        }
73
74
        $registeredMethod = $this->getFromMember($member, $method)
75
            ?: RegisteredMethod::create(['MethodClassName' => get_class($method)]);
76
77
        $registeredMethod->Data = json_encode($data);
78
        $registeredMethod->write();
79
80
        // Add it to the member
81
        $member->RegisteredMFAMethods()->add($registeredMethod);
82
83
        // Define as the default, if none exists yet
84
        if (!$member->getDefaultRegisteredMethod()) {
85
            $member->setDefaultRegisteredMethod($registeredMethod);
86
            $member->write();
87
        }
88
89
        if (!MethodRegistry::create()->isBackupMethod($method)) {
90
            $this->notification->send(
91
                $member,
92
                'SilverStripe/MFA/Email/Notification_register',
93
                [
94
                    'subject' => _t(
95
                        self::class . '.MFAADDED',
96
                        'A multi-factor authentication method was added to your account'
97
                    ),
98
                    'MethodName' => $method->getName(),
99
                ]
100
            );
101
        }
102
103
        $this->extend('onRegisterMethod', $member, $method);
104
105
        return true;
106
    }
107
108
    /**
109
     * Determines if a method can be removed
110
     *
111
     * By default this is false if MFA is required and the method is the last on the Member (besides the backup method)
112
     * but the funcation provides a hook point for extensibility e.g. if an site requires a particular method to be in
113
     * use by a subset of members - admins must use U2F but normal users can use TOTP.
114
     *
115
     * @param Member $member
116
     * @param MethodInterface $method
117
     * @return bool
118
     */
119
    public function canRemoveMethod(Member $member, MethodInterface $method): bool
120
    {
121
        $removable = true;
122
        $backupMethodClass = MethodRegistry::config()->get('default_backup_method');
123
        $remainingMethods = $member->RegisteredMFAMethods()
124
            ->filter('MethodClassName:Not', $backupMethodClass)
125
            ->count();
126
        $mfaIsRequired = Injector::inst()->get(EnforcementManager::class)->isMFARequired();
127
128
        // This is the last method (besides the backup method), and MFA is required
129
        if ($mfaIsRequired && $remainingMethods === 1) {
130
            $removable = false;
131
        }
132
133
        $this->extend(__FUNCTION__, $removable, $member, $method);
134
135
        return $removable;
136
    }
137
138
    /**
139
     * Delete a registration for the given method from the given member, provided it exists. This will also remove a
140
     * registered back-up method if it will leave the member with only the back-up method remaing
141
     *
142
     * @param Member&MemberExtension $member
143
     * @param MethodInterface $method
144
     * @return bool Returns false if the given method is not registered for the member
145
     * @throws \SilverStripe\ORM\ValidationException
146
     */
147
    public function deleteFromMember(Member $member, MethodInterface $method): bool
148
    {
149
        if (!$method || !$this->canRemoveMethod($member, $method)) {
0 ignored issues
show
introduced by
$method is of type SilverStripe\MFA\Method\MethodInterface, thus it always evaluated to true.
Loading history...
150
            return false;
151
        }
152
153
        $registeredMethod = $this->getFromMember($member, $method);
154
        $registeredMethod->delete();
155
156
        $backupRemovedToo = false;
157
158
        $backupMethod = MethodRegistry::config()->get('default_backup_method');
159
        $remainingMethods = $member->RegisteredMFAMethods()->count();
160
        if ($remainingMethods === 2) {
161
            // If there is only one other method (other than backup codes) then set that as the default method
162
            /** @var RegisteredMethod|null $remainingMethodExceptBackup */
163
            $remainingMethodExceptBackup = $member->RegisteredMFAMethods()
164
                ->filter('MethodClassName:Not', $backupMethod)
165
                ->first();
166
167
            if ($remainingMethodExceptBackup) {
168
                $member->setDefaultRegisteredMethod($remainingMethodExceptBackup);
169
                $member->write();
170
            }
171
        } elseif ($remainingMethods === 1) {
172
            // If there is only one method remaining, and that's the configured "backup" method - then delete that too
173
            $remainingMethod = $member->RegisteredMFAMethods()
174
                ->filter('MethodClassName', $backupMethod)
175
                ->first();
176
177
            if ($remainingMethod) {
178
                $remainingMethod->delete();
179
                $backupRemovedToo = true;
180
            }
181
        }
182
183
        $this->notification->send(
184
            $member,
185
            'SilverStripe/MFA/Email/Notification_removed',
186
            [
187
                'subject' => _t(
188
                    self::class . '.MFAREMOVED',
189
                    'A multi-factor authentication method was removed from your account'
190
                ),
191
                'MethodName' => $method->getName(),
192
                'BackupAlsoRemoved' => $backupRemovedToo,
193
            ]
194
        );
195
196
        return true;
197
    }
198
}
199