openstamanager/src/Auth.php

496 lines
15 KiB
PHP
Executable File

<?php
/*
* OpenSTAManager: il software gestionale open source per l'assistenza tecnica e la fatturazione
* Copyright (C) DevCode s.r.l.
*
* 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
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use API\Response as API;
use Models\Group;
use Models\User;
/**
* Classe per la gestione delle utenze.
*
* @since 2.3
*/
class Auth extends Util\Singleton
{
/** @var array Stati previsti dal sistema di autenticazione */
protected static $status = [
'success' => [
'code' => 1,
'message' => 'Login riuscito!',
],
'failed' => [
'code' => 0,
'message' => 'Autenticazione fallita!',
],
'disabled' => [
'code' => 2,
'message' => 'Utente non abilitato!',
],
'unauthorized' => [
'code' => 5,
'message' => "L'utente non ha nessun permesso impostato!",
],
];
/** @var array Opzioni di sicurezza relative all'hashing delle password */
protected static $password_options = [
'algorithm' => PASSWORD_BCRYPT,
'options' => [],
];
/** @var array Opzioni per la protezione contro attacchi brute-force */
protected static $brute_options = [
'attemps' => 3,
'timeout' => 180,
];
/** @var bool Informazioni riguardanti la condizione brute-force */
protected static $is_brute;
/** @var array Informazioni riguardanti l'utente autenticato */
protected $user;
/** @var string Stato del tentativo di accesso */
protected $current_status;
/** @var string|null Nome del primo modulo su cui l'utente ha permessi di navigazione */
protected $first_module;
protected function __construct()
{
$database = database();
if ($database->isInstalled()) {
// Controllo dell'accesso da API
if (API::isAPIRequest()) {
$token = API::getRequest()['token'];
$user = $database->fetchArray('SELECT `id_utente` FROM `zz_tokens` WHERE `enabled` = 1 AND `token` = :token', [
':token' => $token,
]);
$id = !empty($user) ? $user[0]['id_utente'] : null;
}
// Controllo sulla sessione attiva
elseif (!empty($_SESSION['id_utente'])) {
$id = $_SESSION['id_utente'];
}
if (!empty($id)) {
$this->identifyUser($id);
}
$this->saveToSession();
}
}
/**
* Effettua un tentativo di accesso con le credenziali fornite.
*
* @param string $username
* @param string $password
* @param bool $force Forza il login solo tramite username (serve per l'autenticazione con Oauth2)
*
* @return bool
*/
public function attempt($username, $password, $force = false)
{
session_regenerate_id();
// Controllo sulla disponibilità dell'accesso (brute-forcing non in corso)
if (self::isBrute()) {
return false;
}
$database = database();
$log = [];
$log['username'] = $username;
$log['ip'] = get_client_ip();
$status = 'failed';
$users = $database->fetchArray('SELECT id, password, enabled FROM zz_users WHERE username = :username LIMIT 1', [
':username' => $username,
]);
if (!empty($users)) {
$user = $users[0];
if (!empty($user['enabled'])) {
$this->identifyUser($user['id']);
$gruppo = Group::join('zz_users', 'zz_users.idgruppo', '=', 'zz_groups.id')->where('zz_users.id', '=', $user['id'])->first();
$module = $gruppo->id_module_start;
$module = $this->getFirstModule($module);
if ($force) {
// Accesso completato
$log['id_utente'] = $this->user->id;
$status = 'success';
// Salvataggio nella sessione
$this->saveToSession();
} elseif (
$this->isAuthenticated()
&& $this->password_check($password, $user['password'], $user['id'])
&& !empty($module)
) {
// Accesso completato
$log['id_utente'] = $this->user->id;
$status = 'success';
// Salvataggio nella sessione
$this->saveToSession();
} else {
if (empty($module)) {
$status = 'unauthorized';
}
// Logout automatico
$this->destory();
}
} else {
$status = 'disabled';
}
}
// Salvataggio dello stato corrente
$log['stato'] = self::getStatus()[$status]['code'];
$log['user_agent'] = Filter::getPurifier()->purify($_SERVER['HTTP_USER_AGENT']);
$this->current_status = $status;
// Salvataggio del tentativo nel database
$database->insert('zz_logs', $log);
return $this->isAuthenticated();
}
/**
* Controlla se l'utente è autenticato.
*
* @return bool
*/
public function isAuthenticated()
{
return !empty($this->user);
}
/**
* Controlla se l'utente appartiene al gruppo degli Amministratori.
*
* @return bool
*/
public function isAdmin()
{
return $this->isAuthenticated() && !empty($this->user->is_admin);
}
/**
* Restituisce le informazioni riguardanti l'utente autenticato.
*
* @return User
*/
public function getUser()
{
return $this->user;
}
/**
* Restituisce lo stato corrente.
*
* @return string
*/
public function getCurrentStatus()
{
return $this->current_status;
}
/**
* Restituisce il token di accesso all'API per l'utente autenticato.
*
* @return string
*/
public function getToken()
{
$token = null;
if ($this->isAuthenticated()) {
$user = self::user();
$tokens = $user->getApiTokens();
$token = $tokens[0]['token'];
}
return $token;
}
/**
* Distrugge le informazioni riguardanti l'utente autenticato, forzando il logout.
*/
public function destory()
{
if ($this->isAuthenticated() || !empty($_SESSION['id_utente'])) {
$this->user = [];
$this->first_module = null;
session_unset();
session_regenerate_id();
if (!API::isAPIRequest()) {
flash()->clearMessages();
}
}
}
/**
* Restituisce il nome del primo modulo navigabile dall'utente autenticato.
*
* @return string|null
*/
public function getFirstModule($first = null)
{
if (empty($this->first_module)) {
$parameters = [];
$query = 'SELECT `zz_modules`.`id` FROM `zz_modules` WHERE `enabled` = 1';
if (!$this->isAdmin()) {
$group = $this->getUser()['gruppo'];
$query .= ' AND `id` IN (SELECT `idmodule` FROM `zz_permissions` WHERE `idgruppo` = '.(new Group())->getByField('title', $group)." AND `permessi` IN ('r', 'rw'))";
}
$database = database();
$results = $database->fetchArray($query." AND `options` != '' AND `options` != 'menu' AND `options` IS NOT NULL ORDER BY `order` ASC", $parameters);
if (!empty($results)) {
$module = null;
if (empty($first)) {
$first = setting('Prima pagina');
}
if (!in_array($first, array_column($results, 'id'))) {
$module = $results[0]['id'];
} else {
$module = $first;
}
$this->first_module = $module;
}
}
return $this->first_module;
}
/**
* Restituisce l'hashing della password per la relativa memorizzazione nel database.
*
* @param string $password
*
* @return string|bool
*/
public static function hashPassword($password)
{
return password_hash($password, self::$password_options['algorithm'], self::$password_options['options']);
}
/**
* Restituisce l'elenco degli stati del sistema di autenticazione.
*
* @return array
*/
public static function getStatus()
{
return self::$status;
}
/**
* Controlla se l'utente è autenticato.
*
* @return bool
*/
public static function check()
{
return self::getInstance()->isAuthenticated();
}
/**
* Controlla se l'utente appartiene al gruppo degli Amministratori.
*
* @return bool
*/
public static function admin()
{
return self::getInstance()->isAdmin();
}
/**
* Restituisce le informazioni riguardanti l'utente autenticato.
*
* @return User
*/
public static function user()
{
return self::getInstance()->getUser();
}
/**
* Distrugge le informazioni riguardanti l'utente autenticato, forzando il logout.
*/
public static function logout()
{
return self::getInstance()->destory();
}
/**
* Restituisce il nome del primo modulo navigabile dall'utente autenticato.
*
* @return string
*/
public static function firstModule()
{
return self::getInstance()->getFirstModule();
}
/**
* Controlla se sono in corso molti tentativi di accesso (possibile brute-forcing in corso).
*
* @return bool
*/
public static function isBrute()
{
$database = database();
if (!$database->isInstalled() || !$database->tableExists('zz_logs') || Update::isUpdateAvailable()) {
return false;
}
if (!isset(self::$is_brute)) {
$results = $database->fetchArray('SELECT COUNT(*) AS tot FROM zz_logs WHERE ip = :ip AND stato = :state AND DATE_ADD(created_at, INTERVAL :timeout SECOND) >= NOW()', [
':ip' => get_client_ip(),
':state' => self::getStatus()['failed']['code'],
':timeout' => self::$brute_options['timeout'],
]);
self::$is_brute = $results[0]['tot'] > self::$brute_options['attemps'];
}
return self::$is_brute;
}
/**
* Restituisce il tempo di attesa rimanente per lo sblocco automatico dellla protezione contro attacchi brute-force.
*
* @return int
*/
public static function getBruteTimeout()
{
if (!self::isBrute()) {
return 0;
}
$database = database();
$results = $database->fetchArray('SELECT TIME_TO_SEC(TIMEDIFF(DATE_ADD(created_at, INTERVAL '.self::$brute_options['timeout'].' SECOND), NOW())) AS diff FROM zz_logs WHERE ip = :ip AND stato = :state AND DATE_ADD(created_at, INTERVAL :timeout SECOND) >= NOW() ORDER BY created_at DESC LIMIT 1', [
':ip' => get_client_ip(),
':state' => self::getStatus()['failed']['code'],
':timeout' => self::$brute_options['timeout'],
]);
return intval($results[0]['diff']);
}
/**
* Controlla la corrispondenza delle password ed eventualmente effettua un rehashing.
*
* @param string $password
* @param string $hash
* @param int $user_id
*/
protected function password_check($password, $hash, $user_id)
{
$result = false;
$rehash = false;
// Retrocompatibilità
if ($hash == md5($password)) {
$rehash = true;
$result = true;
}
// Nuova versione
if (password_verify($password, $hash)) {
$rehash = password_needs_rehash($hash, self::$password_options['algorithm'], self::$password_options['options']);
$result = true;
}
// Controllo in automatico per futuri cambiamenti dell'algoritmo di password
if ($rehash) {
$database = database();
$database->update('zz_users', ['password' => self::hashPassword($password)], ['id' => $user_id]);
}
return $result;
}
/**
* Memorizza le informazioni riguardanti l'utente all'interno della sessione.
*/
protected function saveToSession()
{
if (session_status() == PHP_SESSION_ACTIVE && $this->isAuthenticated()) {
// Retrocompatibilità
foreach ($this->user as $key => $value) {
$_SESSION[$key] = $value;
}
$_SESSION['id_utente'] = $this->user->id;
$identifier = md5($_SESSION['id_utente'].$_SERVER['HTTP_USER_AGENT']);
if ((empty($_SESSION['last_active']) || time() < $_SESSION['last_active'] + (60 * 60)) && (empty($_SESSION['identifier']) || $_SESSION['identifier'] == $identifier)) {
$_SESSION['last_active'] = time();
$_SESSION['identifier'] = $identifier;
}
}
}
/**
* Identifica l'utente interessato dall'autenticazione.
*
* @param int $user_id
*/
protected function identifyUser($user_id)
{
$database = database();
try {
$results = $database->fetchArray('SELECT `id`, `idanagrafica`, `username`, (SELECT `title` FROM `zz_groups` LEFT JOIN `zz_groups_lang` ON `zz_groups`.`id`=`zz_groups_lang`.`id_record` AND `zz_groups_lang`.`id_lang`='.prepare(Models\Locale::getDefault()->id).' WHERE `zz_groups`.`id` = `zz_users`.`idgruppo`) AS gruppo FROM `zz_users` WHERE `id` = :user_id AND `enabled` = 1 LIMIT 1', [
':user_id' => $user_id,
]);
if (!empty($results)) {
$this->user = User::with('group')->find($user_id);
if (!API::isAPIRequest() && !empty($this->user->reset_token)) {
$this->user->reset_token = null;
$this->user->save();
}
}
} catch (PDOException $e) {
$this->destory();
}
}
}