Completed
Pull Request — master (#6)
by Robbie
03:26 queued 52s
created

triggerBasicAuthProtection()   C

Complexity

Conditions 12
Paths 96

Size

Total Lines 55
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 55
rs 6.8009
c 0
b 0
f 0
cc 12
eloc 31
nc 96
nop 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace CWP\Core\Extension;
4
5
use Exception;
6
use SilverStripe\Control\Director;
7
use SilverStripe\Core\Config\Config;
8
use SilverStripe\Core\Environment;
9
use SilverStripe\Core\Extension;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\Security\BasicAuth;
12
use SilverStripe\Security\Member;
13
use SilverStripe\Security\Permission;
14
use SilverStripe\Security\PermissionProvider;
15
use SilverStripe\Security\Security;
16
use SilverStripe\Subsite\Model\Subsite;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Subsite\Model\Subsite was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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