Passed
Push — master ( f999f4...f378ea )
by
unknown
02:33
created

CwpControllerExtension::getRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
rs 9.4285
cc 2
eloc 3
nc 2
nop 0
1
<?php
2
3
namespace CWP\Core\Extension;
4
5
use Exception;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Control\HTTPRequest;
8
use SilverStripe\Core\Config\Config;
9
use SilverStripe\Core\Environment;
10
use SilverStripe\Core\Extension;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\Security\BasicAuth;
13
use SilverStripe\Security\Member;
14
use SilverStripe\Security\Permission;
15
use SilverStripe\Security\PermissionProvider;
16
use SilverStripe\Security\Security;
17
use SilverStripe\Subsites\Model\Subsite;
18
19
class CwpControllerExtension extends Extension implements PermissionProvider
20
{
21
22
    /**
23
     * Enables SSL redirections - disabling not recommended as it will prevent forcing SSL on admin panel.
24
     *
25
     * @config
26
     * @var bool
27
     */
28
    private static $ssl_redirection_enabled = true;
29
30
    /**
31
     * Specify a domain to redirect the vulnerable areas to.
32
     *
33
     * If left as null, live instance will set this to <instance-id>.cwp.govt.nz via CWP_SECURE_DOMAIN in _config.php.
34
     * This allows us to automatically protect vulnerable areas on live even if the frontend cert is not installed.
35
     *
36
     * Set to false to redirect to https protocol on current domain (e.g. if you have frontend cert).
37
     *
38
     * Set to a domain string (e.g. 'example.com') to force that domain.
39
     *
40
     * @config
41
     * @var string
42
     */
43
    private static $ssl_redirection_force_domain = null;
44
45
    /**
46
     * Enables the BasicAuth protection on all test environments. Disable with caution - it will open up
47
     * all your UAT and test environments to the world.
48
     *
49
     * @config
50
     * @var bool
51
     */
52
    private static $test_basicauth_enabled = true;
53
54
    /**
55
     * Enables the BasicAuth protection on all live environments.
56
     * Useful for securing sites prior to public launch.
57
     *
58
     * @config
59
     * @var bool
60
     */
61
    private static $live_basicauth_enabled = false;
62
63
    /**
64
     * This executes the passed callback with subsite filter disabled,
65
     * then enabled the filter again before returning the callback result
66
     * (or throwing the exception the callback raised)
67
     *
68
     * @param  callback  $callback - The callback to execute
69
     * @return mixed     The result of the callback
70
     * @throws Exception Any exception the callback raised
71
     */
72
    protected function callWithSubsitesDisabled($callback)
73
    {
74
        $rv = null;
75
76
        try {
77
            if (class_exists(Subsite::class)) {
78
                Subsite::disable_subsite_filter(true);
79
            }
80
81
            $rv = call_user_func($callback);
82
        } catch (Exception $e) {
83
            if (class_exists(Subsite::class)) {
84
                Subsite::disable_subsite_filter(false);
85
            }
86
87
            throw $e;
88
        }
89
90
        if (class_exists(Subsite::class)) {
91
            Subsite::disable_subsite_filter(false);
92
        }
93
94
        return $rv;
95
    }
96
97
    /**
98
     * Trigger Basic Auth protection, except when there's a reason to bypass it
99
     *  - The source IP address is in the comma-seperated string in the constant CWP_IP_BYPASS_BASICAUTH
100
     *    (so Pingdom, etc, can access the site)
101
     *  - There is an identifiable member, that member has the ACCESS_UAT_SERVER permission, and they're trying
102
     *    to access a white-list of URLs (so people following a reset password link can reset their password)
103
     */
104
    protected function triggerBasicAuthProtection()
105
    {
106
        $allowWithoutAuth = false;
107
108
        // Allow whitelisting IPs for bypassing the basic auth.
109
        if (Environment::getEnv('CWP_IP_BYPASS_BASICAUTH')) {
110
            $remote = $_SERVER['REMOTE_ADDR'];
111
            $bypass = explode(',', Environment::getEnv('CWP_IP_BYPASS_BASICAUTH'));
112
113
            if (in_array($remote, $bypass)) {
114
                $allowWithoutAuth = true;
115
            }
116
        }
117
118
        /** @var HTTPRequest|null $request */
119
        $request = $this->getRequest();
120
121
        // First, see if we can get a member to act on, either from a changepassword token or the session
122
        if (isset($_REQUEST['m']) && isset($_REQUEST['t'])) {
123
            /** @var Member $member */
124
            $member = Member::get()->filter('ID', (int) $_REQUEST['m'])->first();
125
126
            if (!$member->validateAutoLoginToken($_REQUEST['t'])) {
127
                $member = null;
128
            }
129
        } elseif ($request && $request->getSession()->get('AutoLoginHash')) {
130
            $member = Member::member_from_autologinhash(
131
                $request->getSession()->get('AutoLoginHash')
132
            );
133
        } else {
134
            $member = Security::getCurrentUser();
135
        }
136
137
        // Then, if they have the right permissions, check the allowed URLs
138
        $existingMemberCanAccessUAT = $member && $this->callWithSubsitesDisabled(function () use ($member) {
139
            return Permission::checkMember($member, 'ACCESS_UAT_SERVER');
140
        });
141
142
        if ($existingMemberCanAccessUAT) {
143
            $allowed = array(
144
                '/^Security\/changepassword/',
145
                '/^Security\/ChangePasswordForm/'
146
            );
147
148
            $relativeURL = Director::makeRelative(Director::absoluteURL($_SERVER['REQUEST_URI']));
149
150
            foreach ($allowed as $pattern) {
151
                $allowWithoutAuth = $allowWithoutAuth || preg_match($pattern, $relativeURL);
152
            }
153
        }
154
155
        // Finally if they weren't allowed to bypass Basic Auth, trigger it
156
        if (!$allowWithoutAuth) {
157
            $this->callWithSubsitesDisabled(function () use ($request) {
158
                BasicAuth::requireLogin(
159
                    $request,
160
                    _t(__CLASS__ . '.LoginPrompt', "Please log in with your CMS credentials"),
161
                    'ACCESS_UAT_SERVER',
162
                    true
163
                );
164
            });
165
        }
166
    }
167
168
    /**
169
     * Get the current request, either from an Injector service or from the current controller
170
     *
171
     * @return HTTPRequest|null
172
     */
173
    protected function getRequest()
174
    {
175
        if (Injector::inst()->has(HTTPRequest::class)) {
176
            return Injector::inst()->get(HTTPRequest::class);
177
        }
178
        return $this->owner->getRequest();
179
    }
180
181
    /**
182
     * @return void
183
     */
184
    public function onBeforeInit()
185
    {
186
        // Grab global injectable service to allow testing.
187
        $director = Injector::inst()->get(Director::class);
188
189
        if (Config::inst()->get(__CLASS__, 'ssl_redirection_enabled')) {
190
            // redirect some vulnerable areas to the secure domain
191
            if (!$director::is_https()) {
192
                $forceDomain = Config::inst()->get(__CLASS__, 'ssl_redirection_force_domain');
193
194
                if ($forceDomain) {
195
                    $director::forceSSL(['/^Security/', '/^api/'], $forceDomain);
196
                } else {
197
                    $director::forceSSL(['/^Security/', '/^api/']);
198
                }
199
            }
200
        }
201
202
        if (Config::inst()->get(__CLASS__, 'test_basicauth_enabled')) {
203
            // Turn on Basic Auth in testing mode
204
            if ($director::isTest()) {
205
                $this->triggerBasicAuthProtection();
206
            }
207
        }
208
209
        if (Config::inst()->get(__CLASS__, 'live_basicauth_enabled')) {
210
            // Turn on Basic Auth in live mode
211
            if ($director::isLive()) {
212
                $this->triggerBasicAuthProtection();
213
            }
214
        }
215
    }
216
217
    /**
218
     * @return array
219
     */
220
    public function providePermissions()
221
    {
222
        return [
223
            'ACCESS_UAT_SERVER' => _t(
224
                __CLASS__ . '.UatServerPermission',
225
                'Allow users to use their accounts to access the UAT server'
226
            )
227
        ];
228
    }
229
}
230