2017-11-29 02:40:17 +00:00
< ? php
/**
* SSOAuthorizer.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
2021-02-09 00:29:04 +01:00
* along with this program. If not, see <https://www.gnu.org/licenses/>.
2017-11-29 02:40:17 +00:00
*
* @link https://librenms.org
2021-09-10 20:09:53 +02:00
*
2017-11-29 02:40:17 +00:00
* @copyright 2017 Adam Bishop
* @author Adam Bishop <adam@omega.org.uk>
*/
namespace LibreNMS\Authentication ;
2023-08-28 00:13:40 -05:00
use App\Models\User ;
use Illuminate\Support\Arr ;
2017-11-29 02:40:17 +00:00
use LibreNMS\Config ;
2023-08-28 00:13:40 -05:00
use LibreNMS\Enum\LegacyAuthLevel ;
2017-11-29 02:40:17 +00:00
use LibreNMS\Exceptions\AuthenticationException ;
use LibreNMS\Exceptions\InvalidIpException ;
2019-03-12 23:59:03 -05:00
use LibreNMS\Util\IP ;
2017-11-29 02:40:17 +00:00
/**
* Some functionality in this mechanism is inspired by confluence_http_authenticator (@chauth) and graylog-plugin-auth-sso (@Graylog)
*/
2018-02-06 15:20:34 -06:00
class SSOAuthorizer extends MysqlAuthorizer
2017-11-29 02:40:17 +00:00
{
2021-04-01 17:35:18 +02:00
protected static $HAS_AUTH_USERMANAGEMENT = true ;
protected static $CAN_UPDATE_USER = true ;
protected static $CAN_UPDATE_PASSWORDS = false ;
protected static $AUTH_IS_EXTERNAL = true ;
2017-11-29 02:40:17 +00:00
2019-03-05 00:24:14 -06:00
public function authenticate ( $credentials )
2017-11-29 02:40:17 +00:00
{
2019-03-05 00:24:14 -06:00
if ( empty ( $credentials [ 'username' ])) {
2019-06-23 00:29:12 -05:00
throw new AuthenticationException ( '\'sso.user_attr\' config setting was not found or was empty' );
2017-11-29 02:40:17 +00:00
}
2023-08-28 00:13:40 -05:00
// User has already been approved by the authenticator so if automatic user create/update is enabled, do it
if ( Config :: get ( 'sso.create_users' ) || Config :: get ( 'sso.update_users' )) {
$user = User :: thisAuth () -> firstOrNew ([ 'username' => $credentials [ 'username' ]]);
2017-11-29 02:40:17 +00:00
2023-08-28 00:13:40 -05:00
$create = ! $user -> exists && Config :: get ( 'sso.create_users' );
$update = $user -> exists && Config :: get ( 'sso.update_users' );
2017-11-29 02:40:17 +00:00
2023-08-28 00:13:40 -05:00
if ( $create || $update ) {
$user -> auth_type = LegacyAuth :: getType ();
$user -> can_modify_passwd = 0 ;
$user -> realname = $this -> authSSOGetAttr ( Config :: get ( 'sso.realname_attr' ));
$user -> email = $this -> authSSOGetAttr ( Config :: get ( 'sso.email_attr' ));
$user -> descr = $this -> authSSOGetAttr ( Config :: get ( 'sso.descr_attr' )) ? : 'SSO User' ;
$user -> save ();
}
2017-11-29 02:40:17 +00:00
}
return true ;
}
public function getExternalUsername ()
{
return $this -> authSSOGetAttr ( Config :: get ( 'sso.user_attr' ), '' );
}
/**
* Return an attribute from the configured attribute store.
* Returns null if the attribute cannot be found
*
2021-09-08 23:35:56 +02:00
* @param string $attr The name of the attribute to find
2017-11-29 02:40:17 +00:00
* @return string|null
*/
public function authSSOGetAttr ( $attr , $prefix = 'HTTP_' )
{
// Check attribute originates from a trusted proxy - we check it on every attribute just in case this gets called after initial login
if ( $this -> authSSOProxyTrusted ()) {
// Short circuit everything if the attribute is non-existant or null
if ( empty ( $attr )) {
return null ;
}
$header_key = $prefix . str_replace ( '-' , '_' , strtoupper ( $attr ));
if ( Config :: get ( 'sso.mode' ) === 'header' && array_key_exists ( $header_key , $_SERVER )) {
return $_SERVER [ $header_key ];
} elseif ( Config :: get ( 'sso.mode' ) === 'env' && array_key_exists ( $attr , $_SERVER )) {
return $_SERVER [ $attr ];
} else {
return null ;
}
} else {
2019-06-23 00:29:12 -05:00
throw new AuthenticationException ( '\'sso.trusted_proxies\'] is set in your config, but this connection did not originate from trusted source: ' . $_SERVER [ 'REMOTE_ADDR' ]);
2017-11-29 02:40:17 +00:00
}
}
/**
* Checks to see if the connection originated from a trusted source address stored in the configuration.
* Returns false if the connection is untrusted, true if the connection is trusted, and true if the trusted sources are not defined.
*
* @return bool
*/
public function authSSOProxyTrusted ()
{
// We assume IP is used - if anyone is using a non-ip transport, support will need to be added
if ( Config :: get ( 'sso.trusted_proxies' )) {
try {
// Where did the HTTP connection originate from?
if ( isset ( $_SERVER [ 'REMOTE_ADDR' ])) {
// Do not replace this with a call to authSSOGetAttr
$source = IP :: parse ( $_SERVER [ 'REMOTE_ADDR' ]);
} else {
return false ;
}
2019-10-13 13:40:38 +00:00
$proxies = Config :: get ( 'sso.trusted_proxies' );
2017-11-29 02:40:17 +00:00
2019-10-13 13:40:38 +00:00
if ( is_array ( $proxies )) {
foreach ( $proxies as $value ) {
$proxy = IP :: parse ( $value );
if ( $proxies == '8.8.8.0/25' ) {
2022-09-09 14:08:06 +02:00
dd ( $source -> inNetwork (( string ) $proxy ));
2019-10-13 13:40:38 +00:00
}
2022-09-09 14:08:06 +02:00
if ( $source -> inNetwork (( string ) $proxy )) {
2019-10-13 13:40:38 +00:00
// Proxy matches trusted subnet
return true ;
}
2017-11-29 02:40:17 +00:00
}
}
2024-01-05 05:39:12 +01:00
2017-11-29 02:40:17 +00:00
// No match, proxy is untrusted
return false ;
} catch ( InvalidIpException $e ) {
// Webserver is talking nonsense (or, IPv10 has been deployed, or maybe something weird like a domain socket is in use?)
return false ;
}
}
2024-01-05 05:39:12 +01:00
2017-11-29 02:40:17 +00:00
// Not enabled, trust everything
return true ;
}
/**
* Calculate the privilege level to assign to a user based on the configuration and attributes supplied by the external authenticator.
* Returns an integer if the permission is found, or raises an AuthenticationException if the configuration is not valid.
2023-08-28 00:13:40 -05:00
* Converts the legacy level into a role
2017-11-29 02:40:17 +00:00
*
2023-08-28 00:13:40 -05:00
* @throws AuthenticationException
2017-11-29 02:40:17 +00:00
*/
2023-08-28 23:38:09 -05:00
public function getRoles ( string $username ) : array | false
2017-11-29 02:40:17 +00:00
{
if ( Config :: get ( 'sso.group_strategy' ) === 'attribute' ) {
if ( Config :: get ( 'sso.level_attr' )) {
if ( is_numeric ( $this -> authSSOGetAttr ( Config :: get ( 'sso.level_attr' )))) {
2023-08-28 00:13:40 -05:00
return Arr :: wrap ( LegacyAuthLevel :: tryFrom (( int ) $this -> authSSOGetAttr ( Config :: get ( 'sso.level_attr' ))) ? -> getName ());
2017-11-29 02:40:17 +00:00
} else {
throw new AuthenticationException ( 'group assignment by attribute requested, but httpd is not setting the attribute to a number' );
}
} else {
2019-06-23 00:29:12 -05:00
throw new AuthenticationException ( 'group assignment by attribute requested, but \'sso.level_attr\' not set in your config' );
2017-11-29 02:40:17 +00:00
}
} elseif ( Config :: get ( 'sso.group_strategy' ) === 'map' ) {
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' )) {
2023-08-28 00:13:40 -05:00
return Arr :: wrap ( LegacyAuthLevel :: tryFrom (( int ) $this -> authSSOParseGroups ()) ? -> getName ());
2017-11-29 02:40:17 +00:00
} else {
2019-06-23 00:29:12 -05:00
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' );
2017-11-29 02:40:17 +00:00
}
} elseif ( Config :: get ( 'sso.group_strategy' ) === 'static' ) {
if ( Config :: get ( 'sso.static_level' )) {
2023-08-28 00:13:40 -05:00
return Arr :: wrap ( LegacyAuthLevel :: tryFrom (( int ) Config :: get ( 'sso.static_level' )) ? -> getName ());
2017-11-29 02:40:17 +00:00
} else {
2019-06-23 00:29:12 -05:00
throw new AuthenticationException ( 'group assignment by static level was requested, but \'sso.group_level_map\' was not set in your config' );
2017-11-29 02:40:17 +00:00
}
}
2019-06-23 00:29:12 -05:00
throw new AuthenticationException ( '\'sso.group_strategy\' is not set to one of attribute in your config, map or static - configuration is unsafe' );
2017-11-29 02:40:17 +00:00
}
/**
2021-10-02 08:02:42 -05:00
* Map a user to a permission level based on a table mapping, sso.static_level (default 0) if no matching group is found.
2017-11-29 02:40:17 +00:00
*
* @return int
*/
public function authSSOParseGroups ()
{
// Parse a delimited group list
2022-10-11 11:30:00 +02:00
$groups = explode ( Config :: get ( 'sso.group_delimiter' , ';' ), $this -> authSSOGetAttr ( Config :: get ( 'sso.group_attr' )) ? ? '' );
2017-11-29 02:40:17 +00:00
$valid_groups = [];
// Only consider groups that match the filter expression - this is an optimisation for sites with thousands of groups
if ( Config :: get ( 'sso.group_filter' )) {
foreach ( $groups as $group ) {
if ( preg_match ( Config :: get ( 'sso.group_filter' ), $group )) {
array_push ( $valid_groups , $group );
}
}
$groups = $valid_groups ;
}
2021-10-02 08:02:42 -05:00
$level = ( int ) Config :: get ( 'sso.static_level' , 0 );
2017-11-29 02:40:17 +00:00
$config_map = Config :: get ( 'sso.group_level_map' );
// Find the highest level the user is entitled to
foreach ( $groups as $value ) {
if ( isset ( $config_map [ $value ])) {
$map = $config_map [ $value ];
2023-05-24 22:21:54 +02:00
if ( is_int ( $map ) && $level < $map ) {
2017-11-29 02:40:17 +00:00
$level = $map ;
}
}
}
return $level ;
}
}