Passed
Push — 4.1.1 ( 01ed8a )
by Robbie
09:45
created

BasicAuth::protect_site_if_necessary()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 16
nc 7
nop 1
dl 0
loc 29
rs 6.7272
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security;
4
5
use SilverStripe\Control\Controller;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Control\HTTPRequest;
8
use SilverStripe\Control\HTTPResponse;
9
use SilverStripe\Control\HTTPResponse_Exception;
10
use SilverStripe\Core\Config\Configurable;
11
use SilverStripe\Core\Environment;
12
use SilverStripe\Core\Injector\Injector;
13
use SilverStripe\ORM\Connect\DatabaseException;
14
use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator;
15
16
/**
17
 * Provides an interface to HTTP basic authentication.
18
 *
19
 * This utility class can be used to secure any request with basic authentication.  To do so,
20
 * {@link BasicAuth::requireLogin()} from your Controller's init() method or action handler method.
21
 *
22
 * It also has a function to protect your entire site.  See {@link BasicAuth::protect_entire_site()}
23
 * for more information. You can control this setting on controller-level by using {@link Controller->basicAuthEnabled}.
24
 */
25
class BasicAuth
26
{
27
    use Configurable;
28
29
    /**
30
     * Env var to set to enable basic auth
31
     */
32
    const USE_BASIC_AUTH = 'SS_USE_BASIC_AUTH';
33
34
    /**
35
     * Default permission code
36
     */
37
    const AUTH_PERMISSION = 'ADMIN';
38
39
    /**
40
     * @config
41
     * @var Boolean Flag set by {@link self::protect_entire_site()}
42
     */
43
    private static $entire_site_protected = false;
0 ignored issues
show
introduced by
The private property $entire_site_protected is not used, and could be removed.
Loading history...
44
45
    /**
46
     * Set to true to ignore in CLI mode
47
     *
48
     * @var bool
49
     */
50
    private static $ignore_cli = true;
0 ignored issues
show
introduced by
The private property $ignore_cli is not used, and could be removed.
Loading history...
51
52
    /**
53
     * @config
54
     * @var String|array Holds a {@link Permission} code that is required
55
     * when calling {@link protect_site_if_necessary()}. Set this value through
56
     * {@link protect_entire_site()}.
57
     */
58
    private static $entire_site_protected_code = 'ADMIN';
0 ignored issues
show
introduced by
The private property $entire_site_protected_code is not used, and could be removed.
Loading history...
59
60
    /**
61
     * @config
62
     * @var String Message that shows in the authentication box.
63
     * Set this value through {@link protect_entire_site()}.
64
     */
65
    private static $entire_site_protected_message = 'SilverStripe test website. Use your CMS login.';
0 ignored issues
show
introduced by
The private property $entire_site_protected_message is not used, and could be removed.
Loading history...
66
67
    /**
68
     * Require basic authentication.  Will request a username and password if none is given.
69
     *
70
     * Used by {@link Controller::init()}.
71
     *
72
     *
73
     * @param HTTPRequest $request
74
     * @param string $realm
75
     * @param string|array $permissionCode Optional
76
     * @param boolean $tryUsingSessionLogin If true, then the method with authenticate against the
77
     *  session log-in if those credentials are disabled.
78
     * @return bool|Member
79
     * @throws HTTPResponse_Exception
80
     */
81
    public static function requireLogin(
82
        HTTPRequest $request,
83
        $realm,
84
        $permissionCode = null,
85
        $tryUsingSessionLogin = true
86
    ) {
87
        if ((Director::is_cli() && static::config()->get('ignore_cli'))) {
88
            return true;
89
        }
90
91
        $member = null;
92
93
        try {
94
            if ($request->getHeader('PHP_AUTH_USER') && $request->getHeader('PHP_AUTH_PW')) {
95
                /** @var MemberAuthenticator $authenticator */
96
                $authenticators = Security::singleton()->getApplicableAuthenticators(Authenticator::LOGIN);
97
98
                foreach ($authenticators as $name => $authenticator) {
99
                    $member = $authenticator->authenticate([
100
                        'Email' => $request->getHeader('PHP_AUTH_USER'),
101
                        'Password' => $request->getHeader('PHP_AUTH_PW'),
102
                    ], $request);
103
                    if ($member instanceof Member) {
104
                        break;
105
                    }
106
                }
107
            }
108
        } catch (DatabaseException $e) {
109
            // Database isn't ready, let people in
110
            return true;
111
        }
112
113
        if (!$member && $tryUsingSessionLogin) {
114
            $member = Security::getCurrentUser();
115
        }
116
117
        // If we've failed the authentication mechanism, then show the login form
118
        if (!$member) {
119
            $response = new HTTPResponse(null, 401);
120
            $response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
121
122
            if ($request->getHeader('PHP_AUTH_USER')) {
123
                $response->setBody(
124
                    _t(
125
                        'SilverStripe\\Security\\BasicAuth.ERRORNOTREC',
126
                        "That username / password isn't recognised"
127
                    )
128
                );
129
            } else {
130
                $response->setBody(
131
                    _t(
132
                        'SilverStripe\\Security\\BasicAuth.ENTERINFO',
133
                        'Please enter a username and password.'
134
                    )
135
                );
136
            }
137
138
            // Exception is caught by RequestHandler->handleRequest() and will halt further execution
139
            $e = new HTTPResponse_Exception(null, 401);
140
            $e->setResponse($response);
141
            throw $e;
142
        }
143
144
        if ($permissionCode && !Permission::checkMember($member->ID, $permissionCode)) {
145
            $response = new HTTPResponse(null, 401);
146
            $response->addHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
147
148
            if ($request->getHeader('PHP_AUTH_USER')) {
149
                $response->setBody(
150
                    _t(
151
                        'SilverStripe\\Security\\BasicAuth.ERRORNOTADMIN',
152
                        'That user is not an administrator.'
153
                    )
154
                );
155
            }
156
157
            // Exception is caught by RequestHandler->handleRequest() and will halt further execution
158
            $e = new HTTPResponse_Exception(null, 401);
159
            $e->setResponse($response);
160
            throw $e;
161
        }
162
163
        return $member;
164
    }
165
166
    /**
167
     * Enable protection of the entire site with basic authentication.
168
     *
169
     * This log-in uses the Member database for authentication, but doesn't interfere with the
170
     * regular log-in form. This can be useful for test sites, where you want to hide the site
171
     * away from prying eyes, but still be able to test the regular log-in features of the site.
172
     *
173
     * You can also enable this feature by adding this line to your .env. Set this to a permission
174
     * code you wish to require.
175
     *
176
     * SS_USE_BASIC_AUTH=ADMIN
177
     *
178
     * @param boolean $protect Set this to false to disable protection.
179
     * @param string $code {@link Permission} code that is required from the user.
180
     *  Defaults to "ADMIN". Set to NULL to just require a valid login, regardless
181
     *  of the permission codes a user has.
182
     * @param string $message
183
     */
184
    public static function protect_entire_site($protect = true, $code = self::AUTH_PERMISSION, $message = null)
185
    {
186
        static::config()
187
            ->set('entire_site_protected', $protect)
188
            ->set('entire_site_protected_code', $code);
189
        if ($message) {
190
            static::config()->set('entire_site_protected_message', $message);
191
        }
192
    }
193
194
    /**
195
     * Call {@link BasicAuth::requireLogin()} if {@link BasicAuth::protect_entire_site()} has been called.
196
     * This is a helper function used by {@link Controller::init()}.
197
     *
198
     * If you want to enabled protection (rather than enforcing it),
199
     * please use {@link protect_entire_site()}.
200
     *
201
     * @param HTTPRequest|null $request
202
     * @throws HTTPResponse_Exception
203
     */
204
    public static function protect_site_if_necessary(HTTPRequest $request = null)
205
    {
206
        $config = static::config();
207
208
        // Check if site is protected
209
        if ($config->get('entire_site_protected')) {
210
            $permissionCode = $config->get('entire_site_protected_code');
211
        } elseif (Environment::getEnv(self::USE_BASIC_AUTH)) {
212
            // Convert legacy 1 / true to ADMIN permissions
213
            $permissionCode = Environment::getEnv(self::USE_BASIC_AUTH);
214
            if (!is_string($permissionCode) || is_numeric($permissionCode)) {
215
                $permissionCode = self::AUTH_PERMISSION;
216
            }
217
        } else {
218
            // Not enabled
219
            return;
220
        }
221
222
        // Get request
223
        if (!$request && Injector::inst()->has(HTTPRequest::class)) {
224
            $request = Injector::inst()->get(HTTPRequest::class);
225
        }
226
227
        // Require login
228
        static::requireLogin(
229
            $request,
230
            $config->get('entire_site_protected_message'),
231
            $permissionCode,
232
            false
233
        );
234
    }
235
}
236