Completed
Push — master ( d05a4b...d896c9 )
by Jonathan
01:23
created

Auth::getLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Vectorface\Auth;
4
5
use Psr\Log\LoggerAwareTrait;
6
use Psr\Log\LoggerInterface;
7
use Vectorface\Auth\Plugin\AuthPluginInterface;
8
use Exception;
9
10
/**
11
 * A class representing a loose Authentication and Authorization framework.
12
 *  - Authentication is handled explicitly with login, logout, and verify functions.
13
 *  - Authorization is handled in whatever capacity is implemented by loaded plugins.
14
 *
15
 * Plugin classes can share data using the Auth class itself as a shared source of data.
16
 */
17
class Auth implements \ArrayAccess
18
{
19
    /**
20
     * Provides setLogger method, and protected logger property.
21
     */
22
    use LoggerAwareTrait;
23
24
    /**
25
     * Name of the method called when authenticating.
26
     */
27
    const ACTION_LOGIN = 'login';
28
29
    /**
30
     * Name of the method called when attempting to log out.
31
     */
32
    const ACTION_LOGOUT = 'logout';
33
34
    /**
35
     * Name of the session verification method.
36
     */
37
    const ACTION_VERIFY = 'verify';
38
39
    /**
40
     * A result that has no effect either positive or negative.
41
     */
42
    const RESULT_NOOP = null;
43
44
    /**
45
     * A result that acts as a provisional success; Pass if all other tests pass.
46
     */
47
    const RESULT_SUCCESS = 0;
48
49
    /**
50
     * A result that indicates forced immediate success, bypassing further tests. Should not be used lightly.
51
     */
52
    const RESULT_FORCE = 1;
53
54
    /**
55
     * A result that indicates failure. All failures are immediate.
56
     */
57
    const RESULT_FAILURE = -1;
58
59
    /**
60
     * An array of auth plugins.
61
     *
62
     * @var AuthPluginInterface[]
63
     */
64
    private $plugins = [];
65
66
    /**
67
     * Shared values, accessible using the ArrayAccess interface.
68
     *
69
     * @var mixed[]
70
     */
71
    private $shared = [];
72
73
    /**
74
     * Add a plugin to the Auth module.
75
     *
76
     * @param string|AuthPluginInterface The name of a plugin class to be registered, or a configured instance of a
77
     *                                   security plugin.
78
     * @return bool True if the plugin was added successfully.
79
     */
80 11
    public function addPlugin($plugin)
81
    {
82 11
        if (!($plugin instanceof AuthPluginInterface)) {
83 1
            if (!is_string($plugin)) {
84 1
                return false;
85
            }
86 1
            if (!in_array(AuthPluginInterface::class, class_implements($plugin, false))) {
87
                return false;
88
            }
89 1
            $plugin = new $plugin();
90
        }
91 11
        $plugin->setAuth($this);
92 11
        $this->plugins[] = $plugin;
93 11
        return true;
94
    }
95
96
    /**
97
     * Perform a login based on username and password.
98
     *
99
     * @param string $username A user identifier.
100
     * @param string $password The user's password.
101
     * @return bool True if the login was successful, false otherwise.
102
     * @throws AuthException
103
     */
104 8
    public function login($username, $password)
105
    {
106 8
        return $this->action(self::ACTION_LOGIN, [$username, $password]);
107
    }
108
109
    /**
110
     * Attempt to log the user out.
111
     *
112
     * @throws AuthException
113
     */
114 2
    public function logout()
115
    {
116 2
        return $this->action(self::ACTION_LOGOUT);
117
    }
118
119
    /**
120
     * Verify a user's saved data - check if the user is logged in.
121
     *
122
     * @return bool True if the user's session was verified successfully.
123
     * @throws AuthException
124
     */
125 4
    public function verify()
126
    {
127 4
        return $this->action(self::ACTION_VERIFY);
128
    }
129
130
    /**
131
     * Call an action on a plugin, with particular arguments.
132
     *
133
     * @param AuthPluginInterface $plugin The plugin on which to run the action.
134
     * @param string $action The plugin action, login, logout, or verify.
135
     * @param mixed[] $arguments A list of arguments to pass to the action.
136
     * @return int The result returned by the Auth plugin.
137
     * @throws AuthException
138
     */
139 9
    private function callPlugin(AuthPluginInterface $plugin, $action, $arguments)
140
    {
141
        /* This is a bit of defensive programming; It is not possible to hit this code. */
142
        // @codeCoverageIgnoreStart
143
        if (!in_array($action, array(self::ACTION_LOGIN, self::ACTION_LOGOUT, self::ACTION_VERIFY))) {
144
            throw new AuthException("Attempted to perform unknown action: $action");
145
        }
146
        // @codeCoverageIgnoreEnd
147
148 9
        $result = call_user_func_array([$plugin, $action], $arguments);
149
150
        /* Expected results are an integer or null; Anything else is incorrect. */
151 9
        if (!(is_int($result) || $result === null)) {
152 1
            throw new AuthException(sprintf(
153 1
                "Unknown %s result in %s plugin: %s",
154 1
                $action,
155 1
                get_class($plugin),
156 1
                print_r($result, true)
157
            ));
158
        }
159
160 9
        return $result;
161
    }
162
163
    /**
164
     * Perform a generalized action: login, logout, or verify: Run said function on each plugin in order verifying
165
     * result along the way.
166
     *
167
     * @param string $action The action to be taken. One of login, logout, or verify.
168
     * @param mixed[] $arguments A list of arguments to pass to the action.
169
     * @return bool True if the action was successful, false otherwise.
170
     * @throws AuthException
171
     */
172 9
    private function action($action, $arguments = [])
173
    {
174 9
        $success = false;
175
176 9
        foreach ($this->plugins as $plugin) {
177
            try {
178 9
                $result = $this->callPlugin($plugin, $action, $arguments);
179 1
            } catch (AuthException $e) {
180 1
                throw $e;
181 1
            } catch (Exception $e) {
182 1
                $this->logException($e, "Fatal %s error in %s plugin", $action, get_class($plugin));
183 1
                return false;
184
            }
185
186 9
            if ($result === self::RESULT_SUCCESS /* 0 */) {
187 7
                $success = true;
188 7
            } elseif ($result !== self::RESULT_NOOP) {
189
                /* If not noop or success, possible options are hard fail (<=-1), or hard success (>=1) */
190 9
                return ($result >= self::RESULT_FORCE);
191
            } /* else result is NOOP. Do nothing. Result not defined for plugin. */
192
        }
193 7
        return $success;
194
    }
195
196
    /**
197
     * Passthrough function call for plugins.
198
     *
199
     * @param string $method The name of the method to be called.
200
     * @param array $args An array of arguments to be passed to the method.
201
     * @return mixed Returns whatever the passthrough function returns, or null or error or missing function.
202
     * @throws AuthException
203
     * @noinspection PhpRedundantCatchClauseInspection
204
     */
205 5
    public function __call($method, $args = [])
206
    {
207 5
        foreach ($this->plugins as $plugin) {
208 5
            if (is_callable([$plugin, $method])) {
209
                try {
210 5
                    return call_user_func_array([$plugin, $method], $args);
211 1
                } catch (AuthException $e) {
212 1
                    throw $e;
213 1
                } catch (Exception $e) {
214 5
                    return $this->logException($e, "Exception caught calling %s->%s", get_class($plugin), $method);
215
                }
216
            }
217
        }
218
219 1
        if ($this->logger) {
220 1
            $this->logger->warning(__CLASS__ . ": $method not implemented by any loaded plugin");
221
        }
222 1
        return null;
223
    }
224
225
    /**
226
     * ArrayAccess offsetExists
227
     *
228
     * @link http://php.net/manual/en/arrayaccess.offsetexists.php
229
     * @inheritDoc
230
     */
231 1
    public function offsetExists($offset)
232
    {
233 1
        return isset($this->shared[$offset]);
234
    }
235
236
    /**
237
     * ArrayAccess offsetGet
238
     *
239
     * @link http://php.net/manual/en/arrayaccess.offsetget.php
240
     * @inheritDoc
241
     */
242 1
    public function offsetGet($offset)
243
    {
244 1
        return $this->shared[$offset] ?? null;
245
    }
246
247
    /**
248
     * ArrayAccess offsetSet
249
     *
250
     * @link http://php.net/manual/en/arrayaccess.offsetset.php
251
     * @inheritDoc
252
     */
253 1
    public function offsetSet($offset, $value)
254
    {
255 1
        $this->shared[$offset] = $value;
256 1
    }
257
258
    /**
259
     * ArrayAccess offsetUnset
260
     *
261
     * @link http://php.net/manual/en/arrayaccess.offsetunset.php
262
     * @inheritDoc
263
     */
264 1
    public function offsetUnset($offset)
265
    {
266 1
        unset($this->shared[$offset]);
267 1
    }
268
269
    /**
270
     * Get the logger instance set on this object
271
     *
272
     * @return LoggerInterface The logger for use in this Auth object, or null if not set.
273
     */
274 2
    public function getLogger()
275
    {
276 2
        return $this->logger; /* Defined as part of the LoggerAwareInterface */
277
    }
278
279
    /**
280
     * Log a message and exception in a semi-consistent form.
281
     *
282
     * Logs the message, and appends exception message and location.
283
     *
284
     * @param Exception $exc The exception to log.
285
     * @param string $message The exception message in printf style.
286
     * @param string ... Any number of string parameters corresponding to %s placeholders in the message string.
287
     * @noinspection PhpUnusedLocalVariableInspection
288
     */
289 2
    private function logException(Exception $exc, $message /*, ... */)
290
    {
291 2
        if ($this->logger) {
292 2
            $args = array_slice(func_get_args(), 2);
293 2
            $message .= ": %s (%s@%s)"; /* Append exception info to log string. */
294 2
            $args = array_merge($args, [$exc->getMessage(), $exc->getFile(), $exc->getLine()]);
295 2
            $this->logger->warning(call_user_func_array('sprintf', $args));
296
        }
297 2
    }
298
}
299