2016-10-28 12:27:35 +00:00
< ? php
declare ( strict_types = 1 );
2017-03-28 04:12:12 +00:00
namespace JKingWeb\Arsse ;
2016-10-28 12:27:35 +00:00
class User {
2017-05-19 03:03:33 +00:00
const RIGHTS_NONE = 0 ; // normal user
const RIGHTS_DOMAIN_MANAGER = 25 ; // able to act for any normal users on same domain; cannot elevate other users
const RIGHTS_DOMAIN_ADMIN = 50 ; // able to act for any users on same domain not above themselves; may elevate users on same domain to domain manager or domain admin
const RIGHTS_GLOBAL_MANAGER = 75 ; // able to act for any normal users on any domain; cannot elevate other users
const RIGHTS_GLOBAL_ADMIN = 100 ; // is completely unrestricted
2017-02-16 20:29:42 +00:00
public $id = null ;
protected $u ;
2017-05-11 22:00:35 +00:00
protected $authz = 0 ;
2017-02-16 20:29:42 +00:00
protected $authzSupported = 0 ;
2017-02-28 04:04:13 +00:00
protected $actor = [];
2017-04-07 01:41:21 +00:00
2017-02-16 20:29:42 +00:00
static public function listDrivers () : array {
$sep = \DIRECTORY_SEPARATOR ;
$path = __DIR__ . $sep . " User " . $sep ;
$classes = [];
2017-03-07 23:01:13 +00:00
foreach ( glob ( $path . " * " . $sep . " Driver.php " ) as $file ) {
$name = basename ( dirname ( $file ));
$class = NS_BASE . " User \\ $name\\Driver " ;
$classes [ $class ] = $class :: driverName ();
2017-02-16 20:29:42 +00:00
}
return $classes ;
}
2017-03-28 22:50:00 +00:00
public function __construct () {
$driver = Data :: $conf -> userDriver ;
$this -> u = new $driver ();
2017-02-16 20:29:42 +00:00
$this -> authzSupported = $this -> u -> driverFunctions ( " authorize " );
}
public function __toString () {
if ( $this -> id === null ) $this -> credentials ();
return ( string ) $this -> id ;
}
2017-02-23 04:22:45 +00:00
// checks whether the logged in user is authorized to act for the affected user (used especially when granting rights)
function authorize ( string $affectedUser , string $action , int $newRightsLevel = 0 ) : bool {
// if authorization checks are disabled (either because we're running the installer or the background updater) just return true
2017-05-11 22:00:35 +00:00
if ( ! $this -> authorizationEnabled ()) return true ;
2017-02-23 04:22:45 +00:00
// if we don't have a logged-in user, fetch credentials
if ( $this -> id === null ) $this -> credentials ();
// if the affected user is the actor and the actor is not trying to grant themselves rights, accept the request
2017-03-28 22:50:00 +00:00
if ( $affectedUser == Data :: $user -> id && $action != " userRightsSet " ) return true ;
2017-05-11 22:00:35 +00:00
// if we're authorizing something other than a user function and the affected user is not the actor, make sure the affected user exists
$this -> authorizationEnabled ( false );
if ( Data :: $user -> id != $affectedUser && strpos ( $action , " user " ) !== 0 && ! $this -> exists ( $affectedUser )) throw new User\Exception ( " doesNotExist " , [ " action " => $action , " user " => $affectedUser ]);
$this -> authorizationEnabled ( true );
2017-02-23 04:22:45 +00:00
// get properties of actor if not already available
2017-03-28 22:50:00 +00:00
if ( ! sizeof ( $this -> actor )) $this -> actor = $this -> propertiesGet ( Data :: $user -> id );
2017-05-11 22:00:35 +00:00
$rights = $this -> actor [ " rights " ];
2017-02-23 04:22:45 +00:00
// if actor is a global admin, accept the request
if ( $rights == User\Driver :: RIGHTS_GLOBAL_ADMIN ) return true ;
// if actor is a common user, deny the request
if ( $rights == User\Driver :: RIGHTS_NONE ) return false ;
// if actor is not some other sort of admin, deny the request
if ( ! in_array ( $rights ,[ User\Driver :: RIGHTS_GLOBAL_MANAGER , User\Driver :: RIGHTS_DOMAIN_MANAGER , User\Driver :: RIGHTS_DOMAIN_ADMIN ], true )) return false ;
// if actor is a domain admin/manager and domains don't match, deny the request
2017-03-28 22:50:00 +00:00
if ( Data :: $conf -> userComposeNames && $this -> actor [ " domain " ] && $rights != User\Driver :: RIGHTS_GLOBAL_MANAGER ) {
2017-02-23 04:22:45 +00:00
$test = " @ " . $this -> actor [ " domain " ];
if ( substr ( $affectedUser , - 1 * strlen ( $test )) != $test ) return false ;
}
// certain actions shouldn't check affected user's rights
if ( in_array ( $action , [ " userRightsGet " , " userExists " , " userList " ], true )) return true ;
if ( $action == " userRightsSet " ) {
// setting rights above your own is not allowed
if ( $newRightsLevel > $rights ) return false ;
2017-02-28 15:52:02 +00:00
// setting yourself to rights you already have is harmless and can be allowed
if ( $this -> id == $affectedUser && $newRightsLevel == $rights ) return true ;
// managers can only set their own rights, and only to normal user
if ( in_array ( $rights , [ User\Driver :: RIGHTS_DOMAIN_MANAGER , User\Driver :: RIGHTS_GLOBAL_MANAGER ])) {
if ( $this -> id != $affectedUser || $newRightsLevel != User\Driver :: RIGHTS_NONE ) return false ;
return true ;
}
2017-02-23 04:22:45 +00:00
}
$affectedRights = $this -> rightsGet ( $affectedUser );
2017-02-28 15:52:02 +00:00
// managers can only act on themselves (checked above) or regular users
if ( in_array ( $rights ,[ User\Driver :: RIGHTS_GLOBAL_MANAGER , User\Driver :: RIGHTS_DOMAIN_MANAGER ]) && $affectedRights != User\Driver :: RIGHTS_NONE ) return false ;
2017-02-28 16:19:33 +00:00
// domain admins canot act above themselves
if ( ! in_array ( $affectedRights ,[ User\Driver :: RIGHTS_NONE , User\Driver :: RIGHTS_DOMAIN_MANAGER , User\Driver :: RIGHTS_DOMAIN_ADMIN ])) return false ;
2017-02-23 04:22:45 +00:00
return true ;
}
2017-04-07 01:41:21 +00:00
2017-02-16 20:29:42 +00:00
public function credentials () : array {
if ( $_SERVER [ 'PHP_AUTH_USER' ]) {
$out = [ " user " => $_SERVER [ 'PHP_AUTH_USER' ], " password " => $_SERVER [ 'PHP_AUTH_PW' ]];
} else if ( $_SERVER [ 'REMOTE_USER' ]) {
$out = [ " user " => $_SERVER [ 'REMOTE_USER' ], " password " => " " ];
} else {
$out = [ " user " => " " , " password " => " " ];
}
2017-03-28 22:50:00 +00:00
if ( Data :: $conf -> userComposeNames && $out [ " user " ] != " " ) {
2017-02-16 20:29:42 +00:00
$out [ " user " ] = $this -> composeName ( $out [ " user " ]);
}
$this -> id = $out [ " user " ];
return $out ;
}
public function auth ( string $user = null , string $password = null ) : bool {
if ( $user === null ) {
2017-05-26 17:06:06 +00:00
return $this -> authHTTP ();
2017-02-16 20:29:42 +00:00
} else {
2017-02-28 04:04:13 +00:00
$this -> id = $user ;
2017-02-28 15:52:02 +00:00
$this -> actor = [];
2017-02-20 22:04:13 +00:00
switch ( $this -> u -> driverFunctions ( " auth " )) {
case User\Driver :: FUNC_EXTERNAL :
$out = $this -> u -> auth ( $user , $password );
2017-03-28 22:50:00 +00:00
if ( $out && ! Data :: $db -> userExists ( $user )) $this -> autoProvision ( $user , $password );
2017-02-20 22:04:13 +00:00
return $out ;
case User\Driver :: FUNC_INTERNAL :
return $this -> u -> auth ( $user , $password );
case User\Driver :: FUNCT_NOT_IMPLEMENTED :
return false ;
2017-02-16 20:29:42 +00:00
}
}
}
public function authHTTP () : bool {
2017-05-26 17:06:06 +00:00
$cred = $this -> credentials ();
if ( ! $cred [ " user " ]) return false ;
if ( ! $this -> auth ( $cred [ " user " ], $cred [ " password " ])) return false ;
2017-02-16 20:29:42 +00:00
return true ;
}
public function driverFunctions ( string $function = null ) {
return $this -> u -> driverFunctions ( $function );
}
2017-04-07 01:41:21 +00:00
2017-02-16 20:29:42 +00:00
public function list ( string $domain = null ) : array {
2017-02-20 22:04:13 +00:00
$func = " userList " ;
switch ( $this -> u -> driverFunctions ( $func )) {
case User\Driver :: FUNC_EXTERNAL :
// we handle authorization checks for external drivers
if ( $domain === null ) {
if ( ! $this -> authorize ( " @ " . $domain , $func )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => $func , " user " => $domain ]);
} else {
if ( ! $this -> authorize ( " " , $func )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => $func , " user " => " all users " ]);
}
case User\Driver :: FUNC_INTERNAL :
// internal functions handle their own authorization
return $this -> u -> userList ( $domain );
case User\Driver :: FUNCT_NOT_IMPLEMENTED :
throw new User\ExceptionNotImplemented ( " notImplemented " , [ " action " => $func , " user " => $domain ]);
2017-02-16 20:29:42 +00:00
}
}
public function authorizationEnabled ( bool $setting = null ) : bool {
2017-05-11 22:00:35 +00:00
if ( is_null ( $setting )) return ! $this -> authz ;
$this -> authz += ( $setting ? - 1 : 1 );
if ( $this -> authz < 0 ) $this -> authz = 0 ;
return ! $this -> authz ;
2017-02-16 20:29:42 +00:00
}
2017-04-07 01:41:21 +00:00
2017-02-16 20:29:42 +00:00
public function exists ( string $user ) : bool {
2017-02-20 22:04:13 +00:00
$func = " userExists " ;
switch ( $this -> u -> driverFunctions ( $func )) {
case User\Driver :: FUNC_EXTERNAL :
// we handle authorization checks for external drivers
if ( ! $this -> authorize ( $user , $func )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => $func , " user " => $user ]);
$out = $this -> u -> userExists ( $user );
2017-03-28 22:50:00 +00:00
if ( $out && ! Data :: $db -> userExists ( $user )) $this -> autoProvision ( $user , " " );
2017-02-20 22:04:13 +00:00
return $out ;
case User\Driver :: FUNC_INTERNAL :
// internal functions handle their own authorization
return $this -> u -> userExists ( $user );
case User\Driver :: FUNCT_NOT_IMPLEMENTED :
// throwing an exception here would break all kinds of stuff; we just report that the user exists
return true ;
2017-02-16 20:29:42 +00:00
}
}
2017-02-20 22:04:13 +00:00
public function add ( $user , $password = null ) : string {
$func = " userAdd " ;
switch ( $this -> u -> driverFunctions ( $func )) {
case User\Driver :: FUNC_EXTERNAL :
// we handle authorization checks for external drivers
if ( ! $this -> authorize ( $user , $func )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => $func , " user " => $user ]);
$newPassword = $this -> u -> userAdd ( $user , $password );
// if there was no exception and we don't have the user in the internal database, add it
2017-03-28 22:50:00 +00:00
if ( ! Data :: $db -> userExists ( $user )) $this -> autoProvision ( $user , $newPassword );
2017-02-20 22:04:13 +00:00
return $newPassword ;
case User\Driver :: FUNC_INTERNAL :
// internal functions handle their own authorization
return $this -> u -> userAdd ( $user , $password );
case User\Driver :: FUNCT_NOT_IMPLEMENTED :
throw new User\ExceptionNotImplemented ( " notImplemented " , [ " action " => $func , " user " => $user ]);
2017-02-16 20:29:42 +00:00
}
}
public function remove ( string $user ) : bool {
2017-02-20 22:04:13 +00:00
$func = " userRemove " ;
switch ( $this -> u -> driverFunctions ( $func )) {
case User\Driver :: FUNC_EXTERNAL :
// we handle authorization checks for external drivers
if ( ! $this -> authorize ( $user , $func )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => $func , " user " => $user ]);
$out = $this -> u -> userRemove ( $user );
2017-03-28 22:50:00 +00:00
if ( $out && Data :: $db -> userExists ( $user )) {
2017-02-20 22:04:13 +00:00
// if the user was removed and we have it in our data, remove it there
2017-03-28 22:50:00 +00:00
if ( ! Data :: $db -> userExists ( $user )) Data :: $db -> userRemove ( $user );
2017-02-20 22:04:13 +00:00
}
return $out ;
case User\Driver :: FUNC_INTERNAL :
// internal functions handle their own authorization
return $this -> u -> userRemove ( $user );
case User\Driver :: FUNCT_NOT_IMPLEMENTED :
throw new User\ExceptionNotImplemented ( " notImplemented " , [ " action " => $func , " user " => $user ]);
2017-02-16 20:29:42 +00:00
}
}
2017-02-21 00:04:08 +00:00
public function passwordSet ( string $user , string $newPassword = null , $oldPassword = null ) : string {
2017-02-20 22:04:13 +00:00
$func = " userPasswordSet " ;
switch ( $this -> u -> driverFunctions ( $func )) {
case User\Driver :: FUNC_EXTERNAL :
// we handle authorization checks for external drivers
if ( ! $this -> authorize ( $user , $func )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => $func , " user " => $user ]);
$out = $this -> u -> userPasswordSet ( $user , $newPassword , $oldPassword );
2017-03-28 22:50:00 +00:00
if ( Data :: $db -> userExists ( $user )) {
2017-02-20 22:04:13 +00:00
// if the password change was successful and the user exists, set the internal password to the same value
2017-03-28 22:50:00 +00:00
Data :: $db -> userPasswordSet ( $user , $out );
2017-02-20 22:04:13 +00:00
} else {
// if the user does not exists in the internal database, create it
$this -> autoProvision ( $user , $out );
}
return $out ;
case User\Driver :: FUNC_INTERNAL :
// internal functions handle their own authorization
return $this -> u -> userPasswordSet ( $user , $newPassword );
case User\Driver :: FUNCT_NOT_IMPLEMENTED :
throw new User\ExceptionNotImplemented ( " notImplemented " , [ " action " => $func , " user " => $user ]);
2017-02-16 20:29:42 +00:00
}
}
public function propertiesGet ( string $user ) : array {
2017-02-20 22:04:13 +00:00
// prepare default values
2017-02-16 20:29:42 +00:00
$domain = null ;
2017-03-28 22:50:00 +00:00
if ( Data :: $conf -> userComposeNames ) $domain = substr ( $user , strrpos ( $user , " @ " ) + 1 );
2017-02-16 20:29:42 +00:00
$init = [
" id " => $user ,
" name " => $user ,
" rights " => User\Driver :: RIGHTS_NONE ,
" domain " => $domain
];
2017-02-20 22:04:13 +00:00
$func = " userPropertiesGet " ;
switch ( $this -> u -> driverFunctions ( $func )) {
case User\Driver :: FUNC_EXTERNAL :
// we handle authorization checks for external drivers
if ( ! $this -> authorize ( $user , $func )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => $func , " user " => $user ]);
$out = array_merge ( $init , $this -> u -> userPropertiesGet ( $user ));
// remove password if it is return (not exhaustive, but...)
if ( array_key_exists ( 'password' , $out )) unset ( $out [ 'password' ]);
// if the user does not exist in the internal database, add it
2017-03-28 22:50:00 +00:00
if ( ! Data :: $db -> userExists ( $user )) $this -> autoProvision ( $user , " " , $out );
2017-02-20 22:04:13 +00:00
return $out ;
case User\Driver :: FUNC_INTERNAL :
// internal functions handle their own authorization
return array_merge ( $init , $this -> u -> userPropertiesGet ( $user ));
case User\Driver :: FUNCT_NOT_IMPLEMENTED :
// we can return generic values if the function is not implemented
return $init ;
2017-02-16 20:29:42 +00:00
}
}
public function propertiesSet ( string $user , array $properties ) : array {
2017-02-20 22:04:13 +00:00
// remove from the array any values which should be set specially
2017-02-25 17:59:39 +00:00
foreach ([ 'id' , 'domain' , 'password' , 'rights' ] as $key ) {
2017-02-20 22:04:13 +00:00
if ( array_key_exists ( $key , $properties )) unset ( $properties [ $key ]);
}
$func = " userPropertiesSet " ;
switch ( $this -> u -> driverFunctions ( $func )) {
case User\Driver :: FUNC_EXTERNAL :
// we handle authorization checks for external drivers
if ( ! $this -> authorize ( $user , $func )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => $func , " user " => $user ]);
$out = $this -> u -> userPropertiesSet ( $user , $properties );
2017-03-28 22:50:00 +00:00
if ( Data :: $db -> userExists ( $user )) {
2017-02-20 22:04:13 +00:00
// if the property change was successful and the user exists, set the internal properties to the same values
2017-03-28 22:50:00 +00:00
Data :: $db -> userPropertiesSet ( $user , $out );
2017-02-20 22:04:13 +00:00
} else {
// if the user does not exists in the internal database, create it
$this -> autoProvision ( $user , " " , $out );
}
return $out ;
case User\Driver :: FUNC_INTERNAL :
// internal functions handle their own authorization
return $this -> u -> userPropertiesSet ( $user , $properties );
case User\Driver :: FUNCT_NOT_IMPLEMENTED :
throw new User\ExceptionNotImplemented ( " notImplemented " , [ " action " => $func , " user " => $user ]);
2017-02-16 20:29:42 +00:00
}
}
public function rightsGet ( string $user ) : int {
2017-02-20 22:04:13 +00:00
$func = " userRightsGet " ;
switch ( $this -> u -> driverFunctions ( $func )) {
case User\Driver :: FUNC_EXTERNAL :
// we handle authorization checks for external drivers
if ( ! $this -> authorize ( $user , $func )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => $func , " user " => $user ]);
$out = $this -> u -> userRightsGet ( $user );
// if the user does not exist in the internal database, add it
2017-03-28 22:50:00 +00:00
if ( ! Data :: $db -> userExists ( $user )) $this -> autoProvision ( $user , " " , null , $out );
2017-02-20 22:04:13 +00:00
return $out ;
case User\Driver :: FUNC_INTERNAL :
// internal functions handle their own authorization
return $this -> u -> userRightsGet ( $user );
case User\Driver :: FUNCT_NOT_IMPLEMENTED :
// assume all users are unprivileged
return User\Driver :: RIGHTS_NONE ;
2017-02-16 20:29:42 +00:00
}
}
2017-04-07 01:41:21 +00:00
2017-02-16 20:29:42 +00:00
public function rightsSet ( string $user , int $level ) : bool {
2017-02-20 22:04:13 +00:00
$func = " userRightsSet " ;
switch ( $this -> u -> driverFunctions ( $func )) {
case User\Driver :: FUNC_EXTERNAL :
// we handle authorization checks for external drivers
if ( ! $this -> authorize ( $user , $func )) throw new User\ExceptionAuthz ( " notAuthorized " , [ " action " => $func , " user " => $user ]);
$out = $this -> u -> userRightsSet ( $user , $level );
// if the user does not exist in the internal database, add it
2017-03-28 22:50:00 +00:00
if ( $out && Data :: $db -> userExists ( $user )) {
2017-02-20 22:04:13 +00:00
$authz = $this -> authorizationEnabled ();
$this -> authorizationEnabled ( false );
2017-03-28 22:50:00 +00:00
Data :: $db -> userRightsSet ( $user , $level );
2017-02-20 22:04:13 +00:00
$this -> authorizationEnabled ( $authz );
} else if ( $out ) {
$this -> autoProvision ( $user , " " , null , $level );
}
return $out ;
case User\Driver :: FUNC_INTERNAL :
// internal functions handle their own authorization
return $this -> u -> userRightsSet ( $user , $level );
case User\Driver :: FUNCT_NOT_IMPLEMENTED :
throw new User\ExceptionNotImplemented ( " notImplemented " , [ " action " => $func , " user " => $user ]);
2017-02-16 20:29:42 +00:00
}
}
2017-04-07 01:41:21 +00:00
2017-02-16 20:29:42 +00:00
protected function composeName ( string $user ) : string {
if ( preg_match ( " /.+?@[^@]+ $ / " , $user )) {
return $user ;
} else {
return $user . " @ " . $_SERVER [ 'HTTP_HOST' ];
}
}
2017-02-20 22:04:13 +00:00
protected function autoProvision ( string $user , string $password = null , array $properties = null , int $rights = 0 ) : string {
// temporarily disable authorization checks, to avoid potential problems
$this -> authorizationEnabled ( false );
// create the user
2017-03-28 22:50:00 +00:00
$out = Data :: $db -> userAdd ( $user , $password );
2017-02-20 22:04:13 +00:00
// set the user rights
2017-03-28 22:50:00 +00:00
Data :: $db -> userRightsSet ( $user , $rights );
2017-02-20 22:04:13 +00:00
// set the user properties...
if ( $properties === null ) {
// if nothing is provided but the driver uses an external function, try to get the current values from the external source
try {
2017-03-28 22:50:00 +00:00
if ( $this -> u -> driverFunctions ( " userPropertiesGet " ) == User\Driver :: FUNC_EXTERNAL ) Data :: $db -> userPropertiesSet ( $user , $this -> u -> userPropertiesGet ( $user ));
2017-02-20 22:04:13 +00:00
} catch ( \Throwable $e ) {}
} else {
// otherwise if values are provided, use those
2017-03-28 22:50:00 +00:00
Data :: $db -> userPropertiesSet ( $user , $properties );
2017-02-16 20:29:42 +00:00
}
2017-02-25 17:59:39 +00:00
// re-enable authorization and return
2017-05-12 03:20:10 +00:00
$this -> authorizationEnabled ( true );
2017-02-20 22:04:13 +00:00
return $out ;
2017-02-16 20:29:42 +00:00
}
2016-10-28 12:27:35 +00:00
}