Issues (2963)

LibreNMS/Authentication/SSOAuthorizer.php (1 issue)

1
<?php
2
/**
3
 * SSOAuthorizer.php
4
 *
5
 * -Description-
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 *
20
 * @link       https://librenms.org
21
 *
22
 * @copyright  2017 Adam Bishop
23
 * @author     Adam Bishop <[email protected]>
24
 */
25
26
namespace LibreNMS\Authentication;
27
28
use LibreNMS\Config;
29
use LibreNMS\Exceptions\AuthenticationException;
30
use LibreNMS\Exceptions\InvalidIpException;
31
use LibreNMS\Util\IP;
32
33
/**
34
 * Some functionality in this mechanism is inspired by confluence_http_authenticator (@chauth) and graylog-plugin-auth-sso (@Graylog)
35
 */
36
class SSOAuthorizer extends MysqlAuthorizer
37
{
38
    protected static $HAS_AUTH_USERMANAGEMENT = true;
39
    protected static $CAN_UPDATE_USER = true;
40
    protected static $CAN_UPDATE_PASSWORDS = false;
41
    protected static $AUTH_IS_EXTERNAL = true;
42
43
    public function authenticate($credentials)
44
    {
45
        if (empty($credentials['username'])) {
46
            throw new AuthenticationException('\'sso.user_attr\' config setting was not found or was empty');
47
        }
48
49
        // Build the user's details from attributes
50
        $email = $this->authSSOGetAttr(Config::get('sso.email_attr'));
51
        $realname = $this->authSSOGetAttr(Config::get('sso.realname_attr'));
52
        $description = $this->authSSOGetAttr(Config::get('sso.descr_attr'));
53
        $can_modify_passwd = 0;
54
55
        $level = $this->authSSOCalculateLevel();
56
57
        // User has already been approved by the authenicator so if automatic user create/update is enabled, do it
58
        if (Config::get('sso.create_users') && ! $this->userExists($credentials['username'])) {
59
            $this->addUser($credentials['username'], null, $level, $email, $realname, $can_modify_passwd, $description ? $description : 'SSO User');
60
        } elseif (Config::get('sso.update_users') && $this->userExists($credentials['username'])) {
61
            $this->updateUser($this->getUserid($credentials['username']), $realname, $level, $can_modify_passwd, $email);
62
        }
63
64
        return true;
65
    }
66
67
    public function getExternalUsername()
68
    {
69
        return $this->authSSOGetAttr(Config::get('sso.user_attr'), '');
70
    }
71
72
    /**
73
     * Return an attribute from the configured attribute store.
74
     * Returns null if the attribute cannot be found
75
     *
76
     * @param  string  $attr  The name of the attribute to find
77
     * @return string|null
78
     */
79
    public function authSSOGetAttr($attr, $prefix = 'HTTP_')
80
    {
81
        // Check attribute originates from a trusted proxy - we check it on every attribute just in case this gets called after initial login
82
        if ($this->authSSOProxyTrusted()) {
83
            // Short circuit everything if the attribute is non-existant or null
84
            if (empty($attr)) {
85
                return null;
86
            }
87
88
            $header_key = $prefix . str_replace('-', '_', strtoupper($attr));
89
90
            if (Config::get('sso.mode') === 'header' && array_key_exists($header_key, $_SERVER)) {
91
                return $_SERVER[$header_key];
92
            } elseif (Config::get('sso.mode') === 'env' && array_key_exists($attr, $_SERVER)) {
93
                return $_SERVER[$attr];
94
            } else {
95
                return null;
96
            }
97
        } else {
98
            throw new AuthenticationException('\'sso.trusted_proxies\'] is set in your config, but this connection did not originate from trusted source: ' . $_SERVER['REMOTE_ADDR']);
99
        }
100
    }
101
102
    /**
103
     * Checks to see if the connection originated from a trusted source address stored in the configuration.
104
     * Returns false if the connection is untrusted, true if the connection is trusted, and true if the trusted sources are not defined.
105
     *
106
     * @return bool
107
     */
108
    public function authSSOProxyTrusted()
109
    {
110
        // We assume IP is used - if anyone is using a non-ip transport, support will need to be added
111
        if (Config::get('sso.trusted_proxies')) {
112
            try {
113
                // Where did the HTTP connection originate from?
114
                if (isset($_SERVER['REMOTE_ADDR'])) {
115
                    // Do not replace this with a call to authSSOGetAttr
116
                    $source = IP::parse($_SERVER['REMOTE_ADDR']);
117
                } else {
118
                    return false;
119
                }
120
121
                $proxies = Config::get('sso.trusted_proxies');
122
123
                if (is_array($proxies)) {
124
                    foreach ($proxies as $value) {
125
                        $proxy = IP::parse($value);
126
                        if ($proxies == '8.8.8.0/25') {
127
                            dd($source->innetwork((string) $proxy));
128
                        }
129
130
                        if ($source->innetwork((string) $proxy)) {
131
                            // Proxy matches trusted subnet
132
                            return true;
133
                        }
134
                    }
135
                }
136
                // No match, proxy is untrusted
137
                return false;
138
            } catch (InvalidIpException $e) {
139
                // Webserver is talking nonsense (or, IPv10 has been deployed, or maybe something weird like a domain socket is in use?)
140
                return false;
141
            }
142
        }
143
        // Not enabled, trust everything
144
        return true;
145
    }
146
147
    /**
148
     * Calculate the privilege level to assign to a user based on the configuration and attributes supplied by the external authenticator.
149
     * Returns an integer if the permission is found, or raises an AuthenticationException if the configuration is not valid.
150
     *
151
     * @return int
152
     */
153
    public function authSSOCalculateLevel()
154
    {
155
        if (Config::get('sso.group_strategy') === 'attribute') {
156
            if (Config::get('sso.level_attr')) {
157
                if (is_numeric($this->authSSOGetAttr(Config::get('sso.level_attr')))) {
158
                    return (int) $this->authSSOGetAttr(Config::get('sso.level_attr'));
159
                } else {
160
                    throw new AuthenticationException('group assignment by attribute requested, but httpd is not setting the attribute to a number');
161
                }
162
            } else {
163
                throw new AuthenticationException('group assignment by attribute requested, but \'sso.level_attr\' not set in your config');
164
            }
165
        } elseif (Config::get('sso.group_strategy') === 'map') {
166
            if (Config::get('sso.group_level_map') && is_array(Config::get('sso.group_level_map')) && Config::get('sso.group_delimiter') && Config::get('sso.group_attr')) {
167
                return (int) $this->authSSOParseGroups();
168
            } else {
169
                throw new AuthenticationException('group assignment by level map requested, but \'sso.group_level_map\', \'sso.group_attr\', or \'sso.group_delimiter\' are not set in your config');
170
            }
171
        } elseif (Config::get('sso.group_strategy') === 'static') {
172
            if (Config::get('sso.static_level')) {
173
                return (int) Config::get('sso.static_level');
174
            } else {
175
                throw new AuthenticationException('group assignment by static level was requested, but \'sso.group_level_map\' was not set in your config');
176
            }
177
        }
178
179
        throw new AuthenticationException('\'sso.group_strategy\' is not set to one of attribute in your config, map or static - configuration is unsafe');
180
    }
181
182
    /**
183
     * Map a user to a permission level based on a table mapping, 0 if no matching group is found.
184
     *
185
     * @return int
186
     */
187
    public function authSSOParseGroups()
188
    {
189
        // Parse a delimited group list
190
        $groups = explode(Config::get('sso.group_delimiter', ';'), $this->authSSOGetAttr(Config::get('sso.group_attr')));
191
192
        $valid_groups = [];
193
194
        // Only consider groups that match the filter expression - this is an optimisation for sites with thousands of groups
195
        if (Config::get('sso.group_filter')) {
196
            foreach ($groups as $group) {
197
                if (preg_match(Config::get('sso.group_filter'), $group)) {
0 ignored issues
show
It seems like LibreNMS\Config::get('sso.group_filter') can also be of type null; however, parameter $pattern of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

197
                if (preg_match(/** @scrutinizer ignore-type */ Config::get('sso.group_filter'), $group)) {
Loading history...
198
                    array_push($valid_groups, $group);
199
                }
200
            }
201
202
            $groups = $valid_groups;
203
        }
204
205
        $level = 0;
206
207
        $config_map = Config::get('sso.group_level_map');
208
209
        // Find the highest level the user is entitled to
210
        foreach ($groups as $value) {
211
            if (isset($config_map[$value])) {
212
                $map = $config_map[$value];
213
214
                if (is_integer($map) && $level < $map) {
215
                    $level = $map;
216
                }
217
            }
218
        }
219
220
        return $level;
221
    }
222
}
223