RegisteredMethodManager   A
last analyzed

Complexity

Total Complexity 19

Size/Duplication

Total Lines 179
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 72
c 3
b 0
f 0
dl 0
loc 179
rs 10
wmc 19

5 Methods

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