Issues (102)

src/Service/MethodRegistry.php (3 issues)

Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SilverStripe\MFA\Service;
6
7
use SilverStripe\Core\Config\Configurable;
8
use SilverStripe\Core\Injector\Injectable;
9
use SilverStripe\Core\Injector\Injector;
10
use SilverStripe\MFA\BackupCode\Method;
11
use SilverStripe\MFA\Method\MethodInterface;
12
use UnexpectedValueException;
13
14
/**
15
 * A service class that holds the configuration for enabled MFA methods and facilitates providing these methods
16
 */
17
class MethodRegistry
18
{
19
    use Configurable;
20
    use Injectable;
21
22
    /**
23
     * List of configured MFA methods. These should be class names that implement MethodInterface
24
     *
25
     * @config
26
     * @var string[]
27
     */
28
    private static $methods = [];
0 ignored issues
show
The private property $methods is not used, and could be removed.
Loading history...
29
30
    /**
31
     * A string referring to the classname of the method (implementing SilverStripe\MFA\Method\MethodInterface) that is
32
     * to be used as the back-up method for MFA. This alters the registration of this method to be required - a forced
33
     * registration once the user has registered at least one other method. Additionally it cannot be set as the default
34
     * method for a user to log in with.
35
     *
36
     * @config
37
     * @var string
38
     */
39
    private static $default_backup_method = Method::class;
0 ignored issues
show
The private property $default_backup_method is not used, and could be removed.
Loading history...
40
41
    /**
42
     * Request cache of instantiated method instances
43
     *
44
     * @var MethodInterface[]
45
     */
46
    protected $methodInstances;
47
48
    /**
49
     * Get implementations of all configured methods
50
     *
51
     * @return MethodInterface[]
52
     * @throws UnexpectedValueException When an invalid method is registered
53
     * @throws UnexpectedValueException If a method was registered more than once
54
     * @throws UnexpectedValueException If multiple registered methods share a common URL segment
55
     */
56
    public function getMethods(): array
57
    {
58
        if (is_array($this->methodInstances)) {
0 ignored issues
show
The condition is_array($this->methodInstances) is always true.
Loading history...
59
            return $this->methodInstances;
60
        }
61
62
        $configuredMethods = (array) $this->config()->get('methods');
63
        $configuredMethods = array_filter($configuredMethods);
64
        $this->ensureNoDuplicateMethods($configuredMethods);
65
66
        $allMethods = [];
67
        foreach ($configuredMethods as $method) {
68
            $method = Injector::inst()->get($method);
69
70
            if (!$method instanceof MethodInterface) {
71
                throw new UnexpectedValueException(sprintf(
72
                    'Given method "%s" does not implement %s',
73
                    $method,
74
                    MethodInterface::class
75
                ));
76
            }
77
78
            $allMethods[] = $method;
79
        }
80
        $this->ensureNoDuplicateURLSegments($allMethods);
81
82
        return $this->methodInstances = $allMethods;
83
    }
84
85
    /**
86
     * Helper method to indicate whether any MFA methods are registered
87
     *
88
     * @return bool
89
     */
90
    public function hasMethods(): bool
91
    {
92
        return count($this->getMethods()) > 0;
93
    }
94
95
    /**
96
     * Indicates whether the given method is registered as the back-up method for MFA
97
     *
98
     * @param MethodInterface $method
99
     * @return bool
100
     */
101
    public function isBackupMethod(MethodInterface $method): bool
102
    {
103
        $configuredBackupMethod = $this->config()->get('default_backup_method');
104
        return is_string($configuredBackupMethod) && is_a($method, $configuredBackupMethod);
105
    }
106
107
    /**
108
     * Get the configured backup method
109
     *
110
     * @return MethodInterface|null
111
     */
112
    public function getBackupMethod(): ?MethodInterface
113
    {
114
        foreach ($this->getMethods() as $method) {
115
            if ($this->isBackupMethod($method)) {
116
                return $method;
117
            }
118
        }
119
120
        return null;
121
    }
122
123
    /**
124
     * Fetches a Method by its URL Segment
125
     *
126
     * @param string $segment
127
     * @return MethodInterface|null
128
     */
129
    public function getMethodByURLSegment(string $segment): ?MethodInterface
130
    {
131
        foreach ($this->getMethods() as $method) {
132
            if ($method->getURLSegment() === $segment) {
133
                return $method;
134
            }
135
        }
136
137
        return null;
138
    }
139
140
    /**
141
     * Ensure that attempts to register a method multiple times do not occur
142
     *
143
     * @param array $configuredMethods
144
     * @throws UnexpectedValueException
145
     */
146
    private function ensureNoDuplicateMethods(array $configuredMethods): void
147
    {
148
        $uniqueMethods = array_unique($configuredMethods);
149
        if ($uniqueMethods === $configuredMethods) {
150
            return;
151
        }
152
153
        // Get the method class names that were added more than once and format them into a string so we can
154
        // tell the developer which classes were incorrectly configured
155
        $duplicates = array_unique(array_diff_key($configuredMethods, $uniqueMethods));
156
        $methodNames = implode('; ', $duplicates);
157
        throw new UnexpectedValueException(
158
            'Cannot register MFA methods more than once. Check your config: ' . $methodNames
159
        );
160
    }
161
162
    /**
163
     * Ensure that all registered methods have a unique URLSegment
164
     *
165
     * @param array $allMethods
166
     * @throws UnexpectedValueException
167
     */
168
    private function ensureNoDuplicateURLSegments(array $allMethods): void
169
    {
170
        $allURLSegments = array_map(function (MethodInterface $method) {
171
            return $method->getURLSegment();
172
        }, $allMethods);
173
        $uniqueURLSegments = array_unique($allURLSegments);
174
        if ($allURLSegments === $uniqueURLSegments) {
175
            return;
176
        }
177
178
        // Get the method URL segments that were added more than once and format them into a string so we can
179
        // tell the developer which classes were incorrectly configured
180
        $duplicates = array_unique(array_diff_key($allURLSegments, $uniqueURLSegments));
181
        $urlSegments = implode('; ', $duplicates);
182
        throw new UnexpectedValueException(
183
            'Cannot register multiple MFA methods with the same URL segment: ' . $urlSegments
184
        );
185
    }
186
}
187