Remember me (#4112)
Co-authored-by: Sukhwinder Dhillon <sukhwinder.dhillon@icinga.com>
This commit is contained in:
parent
577e47142e
commit
68acf12407
|
@ -32,7 +32,15 @@ class AccountController extends Controller
|
||||||
'title' => $this->translate('List and configure your own navigation items'),
|
'title' => $this->translate('List and configure your own navigation items'),
|
||||||
'label' => $this->translate('Navigation'),
|
'label' => $this->translate('Navigation'),
|
||||||
'url' => 'navigation'
|
'url' => 'navigation'
|
||||||
));
|
))
|
||||||
|
->add(
|
||||||
|
'devices',
|
||||||
|
array(
|
||||||
|
'title' => $this->translate('List of devices you are logged in'),
|
||||||
|
'label' => $this->translate('My Devices'),
|
||||||
|
'url' => 'my-devices'
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,6 +7,7 @@ use Icinga\Forms\AcknowledgeApplicationStateMessageForm;
|
||||||
use Icinga\Web\Announcement\AnnouncementCookie;
|
use Icinga\Web\Announcement\AnnouncementCookie;
|
||||||
use Icinga\Web\Announcement\AnnouncementIniRepository;
|
use Icinga\Web\Announcement\AnnouncementIniRepository;
|
||||||
use Icinga\Web\Controller;
|
use Icinga\Web\Controller;
|
||||||
|
use Icinga\Web\RememberMe;
|
||||||
use Icinga\Web\Session;
|
use Icinga\Web\Session;
|
||||||
use Icinga\Web\Widget;
|
use Icinga\Web\Widget;
|
||||||
|
|
||||||
|
@ -65,6 +66,8 @@ class ApplicationStateController extends Controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RememberMe::removeExpired();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function summaryAction()
|
public function summaryAction()
|
||||||
|
|
|
@ -5,16 +5,23 @@ namespace Icinga\Controllers;
|
||||||
|
|
||||||
use Icinga\Application\Hook\AuthenticationHook;
|
use Icinga\Application\Hook\AuthenticationHook;
|
||||||
use Icinga\Application\Icinga;
|
use Icinga\Application\Icinga;
|
||||||
|
use Icinga\Application\Logger;
|
||||||
|
use Icinga\Common\Database;
|
||||||
|
use Icinga\Exception\AuthenticationException;
|
||||||
use Icinga\Forms\Authentication\LoginForm;
|
use Icinga\Forms\Authentication\LoginForm;
|
||||||
use Icinga\Web\Controller;
|
use Icinga\Web\Controller;
|
||||||
use Icinga\Web\Helper\CookieHelper;
|
use Icinga\Web\Helper\CookieHelper;
|
||||||
|
use Icinga\Web\RememberMe;
|
||||||
use Icinga\Web\Url;
|
use Icinga\Web\Url;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Application wide controller for authentication
|
* Application wide controller for authentication
|
||||||
*/
|
*/
|
||||||
class AuthenticationController extends Controller
|
class AuthenticationController extends Controller
|
||||||
{
|
{
|
||||||
|
use Database;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
@ -35,11 +42,33 @@ class AuthenticationController extends Controller
|
||||||
$this->redirectNow(Url::fromPath('setup'));
|
$this->redirectNow(Url::fromPath('setup'));
|
||||||
}
|
}
|
||||||
$form = new LoginForm();
|
$form = new LoginForm();
|
||||||
|
|
||||||
|
if (RememberMe::hasCookie() && $this->hasDb()) {
|
||||||
|
$authenticated = false;
|
||||||
|
try {
|
||||||
|
$rememberMeOld = RememberMe::fromCookie();
|
||||||
|
$authenticated = $rememberMeOld->authenticate();
|
||||||
|
if ($authenticated) {
|
||||||
|
$rememberMe = $rememberMeOld->renew();
|
||||||
|
$this->getResponse()->setCookie($rememberMe->getCookie());
|
||||||
|
$rememberMe->persist($rememberMeOld->getAesCrypt()->getIv());
|
||||||
|
}
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
Logger::error("Can't authenticate user via remember me cookie: %s", $e->getMessage());
|
||||||
|
} catch (AuthenticationException $e) {
|
||||||
|
Logger::error($e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $authenticated) {
|
||||||
|
$this->getResponse()->setCookie(RememberMe::forget());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->Auth()->isAuthenticated()) {
|
if ($this->Auth()->isAuthenticated()) {
|
||||||
// Call provided AuthenticationHook(s) when login action is called
|
// Call provided AuthenticationHook(s) when login action is called
|
||||||
// but icinga web user is already authenticated
|
// but icinga web user is already authenticated
|
||||||
AuthenticationHook::triggerLogin($this->Auth()->getUser());
|
AuthenticationHook::triggerLogin($this->Auth()->getUser());
|
||||||
$this->redirectNow($form->getRedirectUrl());
|
$this->redirectNow($this->params->get('redirect', $form->getRedirectUrl()));
|
||||||
}
|
}
|
||||||
if (! $requiresSetup) {
|
if (! $requiresSetup) {
|
||||||
$cookies = new CookieHelper($this->getRequest());
|
$cookies = new CookieHelper($this->getRequest());
|
||||||
|
@ -77,6 +106,16 @@ class AuthenticationController extends Controller
|
||||||
$this->view->layout()->setLayout('external-logout');
|
$this->view->layout()->setLayout('external-logout');
|
||||||
$this->getResponse()->setHttpResponseCode(401);
|
$this->getResponse()->setHttpResponseCode(401);
|
||||||
} else {
|
} else {
|
||||||
|
if (RememberMe::hasCookie() && $this->hasDb()) {
|
||||||
|
try {
|
||||||
|
(new RememberMe())->remove(RememberMe::fromCookie()->getAesCrypt()->getIV());
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getResponse()->setCookie(RememberMe::forget());
|
||||||
|
}
|
||||||
|
|
||||||
$this->redirectToLogin();
|
$this->redirectToLogin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Controllers;
|
||||||
|
|
||||||
|
use Icinga\Common\Database;
|
||||||
|
use Icinga\Web\Notification;
|
||||||
|
use Icinga\Web\RememberMe;
|
||||||
|
use Icinga\Web\RememberMeUserList;
|
||||||
|
use Icinga\Web\RememberMeUserDevicesList;
|
||||||
|
use ipl\Web\Compat\CompatController;
|
||||||
|
use ipl\Web\Url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ManageUserDevicesController
|
||||||
|
*
|
||||||
|
* you need 'application/sessions' permission to use this controller
|
||||||
|
*/
|
||||||
|
class ManageUserDevicesController extends CompatController
|
||||||
|
{
|
||||||
|
use Database;
|
||||||
|
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
$this->assertPermission('application/sessions');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function indexAction()
|
||||||
|
{
|
||||||
|
$this->getTabs()
|
||||||
|
->add(
|
||||||
|
'manage-user-devices',
|
||||||
|
array(
|
||||||
|
'title' => $this->translate('List of users who stay logged in'),
|
||||||
|
'label' => $this->translate('Users'),
|
||||||
|
'url' => 'manage-user-devices',
|
||||||
|
'data-base-target' => '_self'
|
||||||
|
)
|
||||||
|
)->activate('manage-user-devices');
|
||||||
|
|
||||||
|
$usersList = (new RememberMeUserList())
|
||||||
|
->setUsers(RememberMe::getAllUser())
|
||||||
|
->setUrl('manage-user-devices/devices');
|
||||||
|
|
||||||
|
$this->addContent($usersList);
|
||||||
|
|
||||||
|
if (! $this->hasDb()) {
|
||||||
|
Notification::warning(
|
||||||
|
$this->translate("Users can't stay logged in without a database configuration backend")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function devicesAction()
|
||||||
|
{
|
||||||
|
$this->getTabs()
|
||||||
|
->add(
|
||||||
|
'manage-devices',
|
||||||
|
array(
|
||||||
|
'title' => $this->translate('List of devices'),
|
||||||
|
'label' => $this->translate('Devices'),
|
||||||
|
'url' => 'manage-user-devices/devices'
|
||||||
|
)
|
||||||
|
)->activate('manage-devices');
|
||||||
|
|
||||||
|
$name = $this->params->getRequired('name');
|
||||||
|
$data = (new RememberMeUserDevicesList())
|
||||||
|
->setDevicesList(RememberMe::getAllByUsername($name))
|
||||||
|
->setUsername($name)
|
||||||
|
->setUrl('manage-user-devices/delete');
|
||||||
|
|
||||||
|
$this->addContent($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteAction()
|
||||||
|
{
|
||||||
|
(new RememberMe())->removeSpecific($this->params->getRequired('fingerprint'));
|
||||||
|
|
||||||
|
$this->redirectNow(
|
||||||
|
Url::fromPath('manage-user-devices/devices')
|
||||||
|
->addParams(['name' => $this->params->getRequired('name')])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Controllers;
|
||||||
|
|
||||||
|
use Icinga\Common\Database;
|
||||||
|
use Icinga\Web\Notification;
|
||||||
|
use Icinga\Web\RememberMe;
|
||||||
|
use Icinga\Web\RememberMeUserDevicesList;
|
||||||
|
use ipl\Web\Compat\CompatController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MyDevicesController
|
||||||
|
*
|
||||||
|
* this controller shows you all the devices you are logged in
|
||||||
|
*/
|
||||||
|
class MyDevicesController extends CompatController
|
||||||
|
{
|
||||||
|
use Database;
|
||||||
|
|
||||||
|
public function init()
|
||||||
|
{
|
||||||
|
$this->getTabs()
|
||||||
|
->add(
|
||||||
|
'account',
|
||||||
|
array(
|
||||||
|
'title' => $this->translate('Update your account'),
|
||||||
|
'label' => $this->translate('My Account'),
|
||||||
|
'url' => 'account'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->add(
|
||||||
|
'navigation',
|
||||||
|
array(
|
||||||
|
'title' => $this->translate('List and configure your own navigation items'),
|
||||||
|
'label' => $this->translate('Navigation'),
|
||||||
|
'url' => 'navigation'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
->add(
|
||||||
|
'devices',
|
||||||
|
array(
|
||||||
|
'title' => $this->translate('List of devices you are logged in'),
|
||||||
|
'label' => $this->translate('My Devices'),
|
||||||
|
'url' => 'my-devices'
|
||||||
|
)
|
||||||
|
)->activate('devices');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function indexAction()
|
||||||
|
{
|
||||||
|
$name = $this->auth->getUser()->getUsername();
|
||||||
|
|
||||||
|
$data = (new RememberMeUserDevicesList())
|
||||||
|
->setDevicesList(RememberMe::getAllByUsername($name))
|
||||||
|
->setUsername($name)
|
||||||
|
->setUrl('my-devices/delete');
|
||||||
|
|
||||||
|
$this->addContent($data);
|
||||||
|
|
||||||
|
if (! $this->hasDb()) {
|
||||||
|
Notification::warning(
|
||||||
|
$this->translate("Users can't stay logged in without a database configuration backend")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteAction()
|
||||||
|
{
|
||||||
|
(new RememberMe())->removeSpecific($this->params->getRequired('fingerprint'));
|
||||||
|
|
||||||
|
$this->redirectNow('my-devices');
|
||||||
|
}
|
||||||
|
}
|
|
@ -145,6 +145,14 @@ class NavigationController extends Controller
|
||||||
'label' => $this->translate('Navigation'),
|
'label' => $this->translate('Navigation'),
|
||||||
'url' => 'navigation'
|
'url' => 'navigation'
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
->add(
|
||||||
|
'devices',
|
||||||
|
array(
|
||||||
|
'title' => $this->translate('List of devices you are logged in'),
|
||||||
|
'label' => $this->translate('My Devices'),
|
||||||
|
'url' => 'my-devices'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
$this->setupSortControl(
|
$this->setupSortControl(
|
||||||
array(
|
array(
|
||||||
|
|
|
@ -7,8 +7,10 @@ use Icinga\Application\Config;
|
||||||
use Icinga\Application\Hook\AuthenticationHook;
|
use Icinga\Application\Hook\AuthenticationHook;
|
||||||
use Icinga\Authentication\Auth;
|
use Icinga\Authentication\Auth;
|
||||||
use Icinga\Authentication\User\ExternalBackend;
|
use Icinga\Authentication\User\ExternalBackend;
|
||||||
|
use Icinga\Common\Database;
|
||||||
use Icinga\User;
|
use Icinga\User;
|
||||||
use Icinga\Web\Form;
|
use Icinga\Web\Form;
|
||||||
|
use Icinga\Web\RememberMe;
|
||||||
use Icinga\Web\Url;
|
use Icinga\Web\Url;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,6 +18,8 @@ use Icinga\Web\Url;
|
||||||
*/
|
*/
|
||||||
class LoginForm extends Form
|
class LoginForm extends Form
|
||||||
{
|
{
|
||||||
|
use Database;
|
||||||
|
|
||||||
const DEFAULT_CLASSES = 'icinga-controls';
|
const DEFAULT_CLASSES = 'icinga-controls';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,6 +64,19 @@ class LoginForm extends Form
|
||||||
'class' => isset($formData['username']) ? 'autofocus' : ''
|
'class' => isset($formData['username']) ? 'autofocus' : ''
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
$this->addElement(
|
||||||
|
'checkbox',
|
||||||
|
'rememberme',
|
||||||
|
[
|
||||||
|
'label' => $this->translate('Stay logged in'),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
if (! $this->hasDb()) {
|
||||||
|
$this->getElement('rememberme')
|
||||||
|
->setAttrib('disabled', true)
|
||||||
|
->setAttrib('title', "You can't stay logged in without a database configuration backend");
|
||||||
|
}
|
||||||
|
|
||||||
$this->addElement(
|
$this->addElement(
|
||||||
'hidden',
|
'hidden',
|
||||||
'redirect',
|
'redirect',
|
||||||
|
@ -100,6 +117,12 @@ class LoginForm extends Form
|
||||||
$authenticated = $authChain->authenticate($user, $password);
|
$authenticated = $authChain->authenticate($user, $password);
|
||||||
if ($authenticated) {
|
if ($authenticated) {
|
||||||
$auth->setAuthenticated($user);
|
$auth->setAuthenticated($user);
|
||||||
|
if ($this->getElement('rememberme')->isChecked()) {
|
||||||
|
$rememberMe = RememberMe::fromCredentials($user->getUsername(), $password);
|
||||||
|
$this->getResponse()->setCookie($rememberMe->getCookie());
|
||||||
|
$rememberMe->persist();
|
||||||
|
}
|
||||||
|
|
||||||
// Call provided AuthenticationHook(s) after successful login
|
// Call provided AuthenticationHook(s) after successful login
|
||||||
AuthenticationHook::triggerLogin($user);
|
AuthenticationHook::triggerLogin($user);
|
||||||
$this->getResponse()->setRerenderLayout(true);
|
$this->getResponse()->setRerenderLayout(true);
|
||||||
|
|
|
@ -552,6 +552,9 @@ class RoleForm extends RepositoryForm
|
||||||
],
|
],
|
||||||
'user/share/navigation' => [
|
'user/share/navigation' => [
|
||||||
'description' => t('Allow to share navigation items')
|
'description' => t('Allow to share navigation items')
|
||||||
|
],
|
||||||
|
'application/sessions' => [
|
||||||
|
'description' => t('Allow to manage user sessions')
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
CREATE TABLE `icingaweb_rememberme`(
|
||||||
|
id int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
username varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
passphrase varchar(256) NOT NULL,
|
||||||
|
random_iv varchar(24) NOT NULL,
|
||||||
|
http_user_agent text NOT NULL,
|
||||||
|
expires_at timestamp NULL DEFAULT NULL,
|
||||||
|
ctime timestamp NULL DEFAULT NULL,
|
||||||
|
mtime timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
|
@ -40,3 +40,15 @@ CREATE TABLE `icingaweb_user_preference`(
|
||||||
`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
`mtime` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (`username`,`section`,`name`)
|
PRIMARY KEY (`username`,`section`,`name`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
|
||||||
|
CREATE TABLE `icingaweb_rememberme`(
|
||||||
|
id int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
username varchar(254) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||||
|
passphrase varchar(256) NOT NULL,
|
||||||
|
random_iv varchar(24) NOT NULL,
|
||||||
|
http_user_agent text NOT NULL,
|
||||||
|
expires_at timestamp NULL DEFAULT NULL,
|
||||||
|
ctime timestamp NULL DEFAULT NULL,
|
||||||
|
mtime timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
CREATE TABLE "icingaweb_rememberme" (
|
||||||
|
"id" serial,
|
||||||
|
"username" character varying(254) NOT NULL,
|
||||||
|
"passphrase" character varying(256) NOT NULL,
|
||||||
|
"random_iv" character varying(24) NOT NULL,
|
||||||
|
"http_user_agent" text NOT NULL,
|
||||||
|
"expires_at" timestamp NULL DEFAULT NULL,
|
||||||
|
"ctime" timestamp NULL DEFAULT NULL,
|
||||||
|
"mtime" timestamp NULL DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE ONLY "icingaweb_rememberme"
|
||||||
|
ADD CONSTRAINT pk_icingaweb_rememberme
|
||||||
|
PRIMARY KEY (
|
||||||
|
"id"
|
||||||
|
);
|
|
@ -100,3 +100,20 @@ CREATE UNIQUE INDEX idx_icingaweb_user_preference
|
||||||
lower((section)::text),
|
lower((section)::text),
|
||||||
lower((name)::text)
|
lower((name)::text)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "icingaweb_rememberme" (
|
||||||
|
"id" serial,
|
||||||
|
"username" character varying(254) NOT NULL,
|
||||||
|
"passphrase" character varying(256) NOT NULL,
|
||||||
|
"random_iv" character varying(24) NOT NULL,
|
||||||
|
"http_user_agent" text NOT NULL,
|
||||||
|
"expires_at" timestamp NULL DEFAULT NULL,
|
||||||
|
"ctime" timestamp NULL DEFAULT NULL,
|
||||||
|
"mtime" timestamp NULL DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE ONLY "icingaweb_rememberme"
|
||||||
|
ADD CONSTRAINT pk_icingaweb_rememberme
|
||||||
|
PRIMARY KEY (
|
||||||
|
"id"
|
||||||
|
);
|
||||||
|
|
|
@ -7,23 +7,47 @@ use Icinga\Application\Config as IcingaConfig;
|
||||||
use Icinga\Data\ResourceFactory;
|
use Icinga\Data\ResourceFactory;
|
||||||
use ipl\Sql\Config as SqlConfig;
|
use ipl\Sql\Config as SqlConfig;
|
||||||
use ipl\Sql\Connection;
|
use ipl\Sql\Connection;
|
||||||
|
use LogicException;
|
||||||
use PDO;
|
use PDO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trait for accessing the Icinga Web database
|
||||||
|
*/
|
||||||
trait Database
|
trait Database
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Get a connection to the Icinga Web database
|
||||||
|
*
|
||||||
|
* @return Connection
|
||||||
|
*
|
||||||
|
* @throws \Icinga\Exception\ConfigurationError
|
||||||
|
*/
|
||||||
protected function getDb()
|
protected function getDb()
|
||||||
{
|
{
|
||||||
|
if (! $this->hasDb()) {
|
||||||
|
throw new LogicException('Please check if a db instance exists at all');
|
||||||
|
}
|
||||||
|
|
||||||
$config = new SqlConfig(ResourceFactory::getResourceConfig(
|
$config = new SqlConfig(ResourceFactory::getResourceConfig(
|
||||||
IcingaConfig::app()->get('global', 'config_resource')
|
IcingaConfig::app()->get('global', 'config_resource')
|
||||||
));
|
));
|
||||||
|
|
||||||
$config->options = [
|
$config->options = [
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ,
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ,
|
||||||
PDO::MYSQL_ATTR_INIT_COMMAND => "SET SESSION SQL_MODE='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE"
|
PDO::MYSQL_ATTR_INIT_COMMAND => "SET SESSION SQL_MODE='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE"
|
||||||
. ",ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'"
|
. ",ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'"
|
||||||
];
|
];
|
||||||
|
|
||||||
$conn = new Connection($config);
|
return new Connection($config);
|
||||||
|
}
|
||||||
|
|
||||||
return $conn;
|
/**
|
||||||
|
* Check if db exists
|
||||||
|
*
|
||||||
|
* @return bool true if a database was found otherwise false
|
||||||
|
*/
|
||||||
|
protected function hasDb()
|
||||||
|
{
|
||||||
|
return (bool) IcingaConfig::app()->get('global', 'config_resource');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,220 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Crypt;
|
||||||
|
|
||||||
|
use UnexpectedValueException;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data encryption and decryption using symmetric algorithm
|
||||||
|
*
|
||||||
|
* # Example Usage
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
*
|
||||||
|
* // Encryption
|
||||||
|
* $encryptedData = new AesCrypt()->encrypt($data); // Accepts a string
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* // Encrypt and encode to Base64
|
||||||
|
* $encryptedData = (new AesCrypt())->encryptToBase64($data); // Accepts a string
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* // Decryption
|
||||||
|
* $aesCrypt = (new AesCrypt())
|
||||||
|
* ->setTag($tag)
|
||||||
|
* ->setIV($iv)
|
||||||
|
* ->setKey($key);
|
||||||
|
*
|
||||||
|
* $decryptedData = $aesCrypt->decrypt($data);
|
||||||
|
*
|
||||||
|
* // Decode from Base64 and decrypt
|
||||||
|
* $aesCrypt = (new AesCrypt())
|
||||||
|
* ->setTag($tag)
|
||||||
|
* ->setIV($iv)
|
||||||
|
* ->setKey($key);
|
||||||
|
*
|
||||||
|
* $decryptedData = $aesCrypt->->decryptFromBase64($data);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class AesCrypt
|
||||||
|
{
|
||||||
|
/** @var string The encryption key */
|
||||||
|
private $key;
|
||||||
|
|
||||||
|
/** @var string The initialization vector which is not NULL */
|
||||||
|
private $iv;
|
||||||
|
|
||||||
|
/** @var string The authentication tag which is passed by reference when using AEAD cipher mode */
|
||||||
|
private $tag;
|
||||||
|
|
||||||
|
/** @var string The cipher method */
|
||||||
|
private $method = 'aes-128-gcm';
|
||||||
|
|
||||||
|
public function __construct($random_bytes_len = 128)
|
||||||
|
{
|
||||||
|
$len = openssl_cipher_iv_length($this->method);
|
||||||
|
$this->iv = random_bytes($len);
|
||||||
|
$this->key = random_bytes($random_bytes_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the key
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setKey($key)
|
||||||
|
{
|
||||||
|
$this->key = $key;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the key
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @throws RuntimeException If the key is not set
|
||||||
|
*/
|
||||||
|
public function getKey()
|
||||||
|
{
|
||||||
|
if (empty($this->key)) {
|
||||||
|
throw new RuntimeException('No key set');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the IV
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setIV($iv)
|
||||||
|
{
|
||||||
|
$this->iv = $iv;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the IV
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @throws RuntimeException If the IV is not set
|
||||||
|
*/
|
||||||
|
public function getIV()
|
||||||
|
{
|
||||||
|
if (empty($this->iv)) {
|
||||||
|
throw new RuntimeException('No iv set');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->iv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the Tag
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setTag($tag)
|
||||||
|
{
|
||||||
|
if (strlen($tag) !== 16) {
|
||||||
|
throw new UnexpectedValueException(sprintf(
|
||||||
|
'expects tag length to be 16, got instead %s',
|
||||||
|
strlen($tag)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->tag = $tag;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Tag
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @throws RuntimeException If the Tag is not set
|
||||||
|
*/
|
||||||
|
public function getTag()
|
||||||
|
{
|
||||||
|
if (empty($this->tag)) {
|
||||||
|
throw new RuntimeException('No tag set');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt the given data using the key, iv and tag
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
* @throws RuntimeException If decryption fails
|
||||||
|
*/
|
||||||
|
public function decrypt($data)
|
||||||
|
{
|
||||||
|
$decrypt = openssl_decrypt($data, $this->method, $this->getKey(), 0, $this->getIV(), $this->getTag());
|
||||||
|
if (is_bool($decrypt) && $decrypt === false) {
|
||||||
|
throw new RuntimeException('Decryption failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decrypt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode from Base64 and decrypt the given data using the key, iv and tag
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
*
|
||||||
|
* @return string decrypted data
|
||||||
|
*
|
||||||
|
* @throws RuntimeException If decryption fails
|
||||||
|
*/
|
||||||
|
public function decryptFromBase64($data)
|
||||||
|
{
|
||||||
|
return $this->decrypt(base64_decode($data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt the given data using the key, iv and tag
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
*
|
||||||
|
* @return string encrypted data
|
||||||
|
*
|
||||||
|
* @throws RuntimeException If decryption fails
|
||||||
|
*/
|
||||||
|
public function encrypt($data)
|
||||||
|
{
|
||||||
|
$encrypt = openssl_encrypt($data, $this->method, $this->getkey(), 0, $this->getIV(), $this->tag);
|
||||||
|
|
||||||
|
if (is_bool($encrypt) && $encrypt === false) {
|
||||||
|
throw new RuntimeException('Encryption failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $encrypt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt the given string using the the key, iv, tag and encode to Base64
|
||||||
|
*
|
||||||
|
* @param string $data
|
||||||
|
*
|
||||||
|
* @return string encrypted and encoded to Base64 data
|
||||||
|
*
|
||||||
|
* @throws RuntimeException If encryption fails
|
||||||
|
*/
|
||||||
|
public function encryptToBase64($data)
|
||||||
|
{
|
||||||
|
return base64_encode($this->encrypt($data));
|
||||||
|
}
|
||||||
|
}
|
|
@ -262,4 +262,38 @@ class Cookie
|
||||||
$this->value = $value;
|
$this->value = $value;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create invalidation cookie
|
||||||
|
*
|
||||||
|
* This method clones the current cookie and sets its value to null and expire time to 1.
|
||||||
|
* That way, the cookie removes itself when it has been sent to and processed by the client.
|
||||||
|
*
|
||||||
|
* We're cloning the current cookie in order to meet the [RFC6265 spec](https://tools.ietf.org/search/rfc6265)
|
||||||
|
* regarding the `Path` and `Domain` attribute:
|
||||||
|
*
|
||||||
|
* > Finally, to remove a cookie, the server returns a Set-Cookie header with an expiration date in the past.
|
||||||
|
* > The server will be successful in removing the cookie only if the Path and the Domain attribute in the
|
||||||
|
* > Set-Cookie header match the values used when the cookie was created.
|
||||||
|
*
|
||||||
|
* Note that the cookie has to be sent to the client.
|
||||||
|
*
|
||||||
|
* # Example Usage
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* $response->setCookie(
|
||||||
|
* $cookie->forgetMe()
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public function forgetMe()
|
||||||
|
{
|
||||||
|
$forgetMe = clone $this;
|
||||||
|
|
||||||
|
return $forgetMe
|
||||||
|
->setValue(null)
|
||||||
|
->setExpire(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,14 @@ class Menu extends Navigation
|
||||||
'label' => t('Announcements'),
|
'label' => t('Announcements'),
|
||||||
'url' => 'announcements',
|
'url' => 'announcements',
|
||||||
'priority' => 720
|
'priority' => 720
|
||||||
|
],
|
||||||
|
'sessions' => [
|
||||||
|
'icon' => 'host',
|
||||||
|
'description' => t('List of users who stay logged in'),
|
||||||
|
'label' => t('User Sessions'),
|
||||||
|
'permission' => 'application/sessions',
|
||||||
|
'url' => 'manage-user-devices',
|
||||||
|
'priority' => 730
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -0,0 +1,335 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Web;
|
||||||
|
|
||||||
|
use Icinga\Application\Config;
|
||||||
|
use Icinga\Authentication\Auth;
|
||||||
|
use Icinga\Crypt\AesCrypt;
|
||||||
|
use Icinga\Common\Database;
|
||||||
|
use Icinga\User;
|
||||||
|
use ipl\Sql\Expression;
|
||||||
|
use ipl\Sql\Select;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remember me component
|
||||||
|
*
|
||||||
|
* Retains credentials for 30 days by default in order to stay signed in even after the session is closed.
|
||||||
|
*/
|
||||||
|
class RememberMe
|
||||||
|
{
|
||||||
|
use Database;
|
||||||
|
|
||||||
|
/** @var string Cookie name */
|
||||||
|
const COOKIE = 'icingaweb2-remember-me';
|
||||||
|
|
||||||
|
/** @var string Database table name */
|
||||||
|
const TABLE = 'icingaweb_rememberme';
|
||||||
|
|
||||||
|
/** @var string Encrypted password of the user */
|
||||||
|
protected $encryptedPassword;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $username;
|
||||||
|
|
||||||
|
/** @var AesCrypt Instance for encrypting/decrypting the credentials */
|
||||||
|
protected $aesCrypt;
|
||||||
|
|
||||||
|
/** @var int Timestamp when the remember me cookie expires */
|
||||||
|
protected $expiresAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether the remember cookie is set
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function hasCookie()
|
||||||
|
{
|
||||||
|
return isset($_COOKIE[static::COOKIE]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unset the remember me cookie from PHP's `$_COOKIE` superglobal and return the invalidation cookie
|
||||||
|
*
|
||||||
|
* @return Cookie Cookie which has to be sent to client in oder to remove the remember me cookie
|
||||||
|
*/
|
||||||
|
public static function forget()
|
||||||
|
{
|
||||||
|
unset($_COOKIE[static::COOKIE]);
|
||||||
|
|
||||||
|
return (new Cookie(static::COOKIE))
|
||||||
|
->setHttpOnly(true)
|
||||||
|
->forgetMe();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the remember me component from the remember me cookie
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public static function fromCookie()
|
||||||
|
{
|
||||||
|
$data = explode('|', $_COOKIE[static::COOKIE]);
|
||||||
|
$iv = base64_decode(array_pop($data));
|
||||||
|
$tag = base64_decode(array_pop($data));
|
||||||
|
|
||||||
|
$select = (new Select())
|
||||||
|
->from(static::TABLE)
|
||||||
|
->columns('*')
|
||||||
|
->where(['random_iv = ?' => bin2hex($iv)]);
|
||||||
|
|
||||||
|
$rememberMe = new static();
|
||||||
|
$rs = $rememberMe->getDb()->select($select)->fetch();
|
||||||
|
|
||||||
|
if (! $rs) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
"No database entry found for IV '%s'",
|
||||||
|
bin2hex($iv)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$rememberMe->aesCrypt = (new AesCrypt())
|
||||||
|
->setKey(hex2bin($rs->passphrase))
|
||||||
|
->setTag($tag)
|
||||||
|
->setIV($iv);
|
||||||
|
$rememberMe->username = $rs->username;
|
||||||
|
$rememberMe->encryptedPassword = $data[0];
|
||||||
|
|
||||||
|
return $rememberMe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the remember me component from the given username and password
|
||||||
|
*
|
||||||
|
* @param string $username
|
||||||
|
* @param string $password
|
||||||
|
*
|
||||||
|
* @return static
|
||||||
|
*/
|
||||||
|
public static function fromCredentials($username, $password)
|
||||||
|
{
|
||||||
|
$aesCrypt = new AesCrypt();
|
||||||
|
$rememberMe = new static();
|
||||||
|
$rememberMe->encryptedPassword = $aesCrypt->encryptToBase64($password);
|
||||||
|
$rememberMe->username = $username;
|
||||||
|
$rememberMe->aesCrypt = $aesCrypt;
|
||||||
|
|
||||||
|
return $rememberMe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove expired remember me information from the database
|
||||||
|
*/
|
||||||
|
public static function removeExpired()
|
||||||
|
{
|
||||||
|
$rememberMe = new static();
|
||||||
|
if (! $rememberMe->hasDb()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rememberMe->getDb()->delete(static::TABLE, [
|
||||||
|
'expires_at < NOW()'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the remember me cookie
|
||||||
|
*
|
||||||
|
* @return Cookie
|
||||||
|
*/
|
||||||
|
public function getCookie()
|
||||||
|
{
|
||||||
|
return (new Cookie(static::COOKIE))
|
||||||
|
->setExpire($this->getExpiresAt())
|
||||||
|
->setHttpOnly(true)
|
||||||
|
->setValue(implode('|', [
|
||||||
|
$this->encryptedPassword,
|
||||||
|
base64_encode($this->aesCrypt->getTag()),
|
||||||
|
base64_encode($this->aesCrypt->getIV()),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the timestamp when the cookie expires
|
||||||
|
*
|
||||||
|
* Defaults to now plus 30 days, if not set via {@link setExpiresAt()}.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getExpiresAt()
|
||||||
|
{
|
||||||
|
if ($this->expiresAt === null) {
|
||||||
|
$this->expiresAt = time() + 60 * 60 * 24 * 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the timestamp when the cookie expires
|
||||||
|
*
|
||||||
|
* @param int $expiresAt
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setExpiresAt($expiresAt)
|
||||||
|
{
|
||||||
|
$this->expiresAt = $expiresAt;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authenticate via the remember me cookie
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
* @throws \Icinga\Exception\AuthenticationException
|
||||||
|
*/
|
||||||
|
public function authenticate()
|
||||||
|
{
|
||||||
|
$password = $this->aesCrypt->decryptFromBase64($this->encryptedPassword);
|
||||||
|
$auth = Auth::getInstance();
|
||||||
|
$authChain = $auth->getAuthChain();
|
||||||
|
$authChain->setSkipExternalBackends(true);
|
||||||
|
$user = new User($this->username);
|
||||||
|
if (! $user->hasDomain()) {
|
||||||
|
$user->setDomain(Config::app()->get('authentication', 'default_domain'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$authenticated = $authChain->authenticate($user, $password);
|
||||||
|
if ($authenticated) {
|
||||||
|
$auth->setAuthenticated($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist the remember me information into the database
|
||||||
|
*
|
||||||
|
* Any previous stored information is automatically removed.
|
||||||
|
*
|
||||||
|
* @param string|null $iv
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function persist($iv = null)
|
||||||
|
{
|
||||||
|
if ($iv) {
|
||||||
|
$this->remove($iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getDb()->insert(static::TABLE, [
|
||||||
|
'username' => $this->username,
|
||||||
|
'passphrase' => bin2hex($this->aesCrypt->getKey()),
|
||||||
|
'random_iv' => bin2hex($this->aesCrypt->getIV()),
|
||||||
|
'http_user_agent' => (new UserAgent)->getAgent(),
|
||||||
|
'expires_at' => date('Y-m-d H:i:s', $this->getExpiresAt()),
|
||||||
|
'ctime' => new Expression('NOW()'),
|
||||||
|
'mtime' => new Expression('NOW()')
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove remember me information from the database
|
||||||
|
*
|
||||||
|
* @param string $iv
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function remove($iv)
|
||||||
|
{
|
||||||
|
$this->getDb()->delete(static::TABLE, [
|
||||||
|
'random_iv = ?' => bin2hex($iv)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create renewed remember me cookie
|
||||||
|
*
|
||||||
|
* @return static New remember me cookie which has to be sent to the client
|
||||||
|
*/
|
||||||
|
public function renew()
|
||||||
|
{
|
||||||
|
return static::fromCredentials(
|
||||||
|
$this->username,
|
||||||
|
$this->aesCrypt->decryptFromBase64($this->encryptedPassword)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove specific remember me information from the database
|
||||||
|
*
|
||||||
|
* @param string $username
|
||||||
|
*
|
||||||
|
* @param $iv
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function removeSpecific($iv)
|
||||||
|
{
|
||||||
|
$this->getDb()->delete(static::TABLE, [
|
||||||
|
'random_iv = ?' => $iv
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all users using rememberme cookie
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getAllUser()
|
||||||
|
{
|
||||||
|
$rememberMe = new static();
|
||||||
|
if (! $rememberMe->hasDb()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$select = (new Select())
|
||||||
|
->from(static::TABLE)
|
||||||
|
->columns('username')
|
||||||
|
->groupBy('username');
|
||||||
|
|
||||||
|
return $rememberMe->getDb()->select($select)->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all rememberme cookies of the given user
|
||||||
|
*
|
||||||
|
* @param $username
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getAllByUsername($username)
|
||||||
|
{
|
||||||
|
$rememberMe = new static();
|
||||||
|
if (! $rememberMe->hasDb()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$select = (new Select())
|
||||||
|
->from(static::TABLE)
|
||||||
|
->columns(['http_user_agent', 'random_iv'])
|
||||||
|
->where(['username = ?' => $username]);
|
||||||
|
|
||||||
|
return $rememberMe->getDb()->select($select)->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the encrypton/decryption instance
|
||||||
|
*
|
||||||
|
* @return AesCrypt
|
||||||
|
*/
|
||||||
|
public function getAesCrypt()
|
||||||
|
{
|
||||||
|
return $this->aesCrypt;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Web;
|
||||||
|
|
||||||
|
use ipl\Html\BaseHtmlElement;
|
||||||
|
use ipl\Html\Html;
|
||||||
|
use ipl\Web\Url;
|
||||||
|
use ipl\Web\Widget\Icon;
|
||||||
|
use ipl\Web\Widget\Link;
|
||||||
|
|
||||||
|
class RememberMeUserDevicesList extends BaseHtmlElement
|
||||||
|
{
|
||||||
|
protected $tag = 'table';
|
||||||
|
|
||||||
|
protected $defaultAttributes = [
|
||||||
|
'class' => 'common-table',
|
||||||
|
'data-base-target' => '_self'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $devicesList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getUrl()
|
||||||
|
{
|
||||||
|
return $this->url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $url
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setUrl($url)
|
||||||
|
{
|
||||||
|
$this->url = $url;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getUsername()
|
||||||
|
{
|
||||||
|
return $this->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $username
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setUsername($username)
|
||||||
|
{
|
||||||
|
$this->username = $username;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array List of devices. Each device contains user agent and fingerprint string
|
||||||
|
*/
|
||||||
|
public function getDevicesList()
|
||||||
|
{
|
||||||
|
return $this->devicesList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $devicesList
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setDevicesList($devicesList)
|
||||||
|
{
|
||||||
|
$this->devicesList = $devicesList;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assemble()
|
||||||
|
{
|
||||||
|
$thead = Html::tag('thead');
|
||||||
|
$theadRow = Html::tag('tr')
|
||||||
|
->add(Html::tag(
|
||||||
|
'th',
|
||||||
|
sprintf(t('List of devices and browsers %s is currently logged in:'), $this->getUsername())
|
||||||
|
));
|
||||||
|
|
||||||
|
$thead->add($theadRow);
|
||||||
|
|
||||||
|
$head = Html::tag('tr')
|
||||||
|
->add(Html::tag('th', t('OS')))
|
||||||
|
->add(Html::tag('th', t('Browser')))
|
||||||
|
->add(Html::tag('th', t('Fingerprint')));
|
||||||
|
|
||||||
|
$thead->add($head);
|
||||||
|
$tbody = Html::tag('tbody');
|
||||||
|
|
||||||
|
if (empty($this->getDevicesList())) {
|
||||||
|
$tbody->add(Html::tag('td', t('No device found')));
|
||||||
|
} else {
|
||||||
|
foreach ($this->getDevicesList() as $device) {
|
||||||
|
$agent = new UserAgent($device);
|
||||||
|
$element = Html::tag('tr')
|
||||||
|
->add(Html::tag('td', $agent->getOs()))
|
||||||
|
->add(Html::tag('td', $agent->getBrowser()))
|
||||||
|
->add(Html::tag('td', $device->random_iv));
|
||||||
|
|
||||||
|
$link = (new Link(
|
||||||
|
new Icon('trash'),
|
||||||
|
Url::fromPath($this->getUrl())
|
||||||
|
->addParams(
|
||||||
|
[
|
||||||
|
'name' => $this->getUsername(),
|
||||||
|
'fingerprint' => $device->random_iv,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
$element->add(Html::tag('td', $link));
|
||||||
|
$tbody->add($element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->add($thead);
|
||||||
|
$this->add($tbody);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Web;
|
||||||
|
|
||||||
|
use ipl\Html\BaseHtmlElement;
|
||||||
|
use ipl\Html\Html;
|
||||||
|
use ipl\Web\Url;
|
||||||
|
use ipl\Web\Widget\Link;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class RememberMeUserList
|
||||||
|
*
|
||||||
|
* @package Icinga\Web
|
||||||
|
*/
|
||||||
|
class RememberMeUserList extends BaseHtmlElement
|
||||||
|
{
|
||||||
|
protected $tag = 'table';
|
||||||
|
|
||||||
|
protected $defaultAttributes = [
|
||||||
|
'class' => 'common-table table-row-selectable',
|
||||||
|
'data-base-target' => '_next',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $users;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getUrl()
|
||||||
|
{
|
||||||
|
return $this->url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $url
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setUrl($url)
|
||||||
|
{
|
||||||
|
$this->url = $url;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getUsers()
|
||||||
|
{
|
||||||
|
return $this->users;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $users
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setUsers($users)
|
||||||
|
{
|
||||||
|
$this->users = $users;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assemble()
|
||||||
|
{
|
||||||
|
$thead = Html::tag('thead');
|
||||||
|
$theadRow = Html::tag('tr')
|
||||||
|
->add(Html::tag(
|
||||||
|
'th',
|
||||||
|
t('List of users who stay logged in')
|
||||||
|
));
|
||||||
|
|
||||||
|
$thead->add($theadRow);
|
||||||
|
$tbody = Html::tag('tbody');
|
||||||
|
|
||||||
|
if (empty($this->getUsers())) {
|
||||||
|
$tbody->add(Html::tag('td', t('No user found')));
|
||||||
|
} else {
|
||||||
|
foreach ($this->getUsers() as $user) {
|
||||||
|
$element = Html::tag('tr');
|
||||||
|
$link = new Link(
|
||||||
|
$user->username,
|
||||||
|
Url::fromPath($this->getUrl())->addParams(['name' => $user->username]),
|
||||||
|
['title' => sprintf(t('Device list of %s'), $user->username)]
|
||||||
|
);
|
||||||
|
|
||||||
|
$element->add(Html::tag('td', $link));
|
||||||
|
$tbody->add($element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->add($thead);
|
||||||
|
$this->add($tbody);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
<?php
|
||||||
|
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
|
||||||
|
|
||||||
|
namespace Icinga\Web;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class UserAgent
|
||||||
|
*
|
||||||
|
* This class helps to get user agent information like OS type and browser name
|
||||||
|
*
|
||||||
|
* @package Icinga\Web
|
||||||
|
*/
|
||||||
|
class UserAgent
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* $_SERVER['HTTP_USER_AGENT'] output string
|
||||||
|
*
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
private $agent;
|
||||||
|
|
||||||
|
public function __construct($agent = null)
|
||||||
|
{
|
||||||
|
$this->agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : null;
|
||||||
|
|
||||||
|
if ($agent) {
|
||||||
|
$this->agent = $agent->http_user_agent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return $_SERVER['HTTP_USER_AGENT'] output string of given or current device
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getAgent()
|
||||||
|
{
|
||||||
|
return $this->agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Browser name
|
||||||
|
*
|
||||||
|
* @return string Browser name or unknown if not found
|
||||||
|
*/
|
||||||
|
public function getBrowser()
|
||||||
|
{
|
||||||
|
// key => regex value
|
||||||
|
$browsers = [
|
||||||
|
"Internet Explorer" => "/MSIE(.*)/i",
|
||||||
|
"Seamonkey" => "/Seamonkey(.*)/i",
|
||||||
|
"MS Edge" => "/Edg(.*)/i",
|
||||||
|
"Opera" => "/Opera(.*)/i",
|
||||||
|
"Opera Browser" => "/OPR(.*)/i",
|
||||||
|
"Chromium" => "/Chromium(.*)/i",
|
||||||
|
"Firefox" => "/Firefox(.*)/i",
|
||||||
|
"Google Chrome" => "/Chrome(.*)/i",
|
||||||
|
"Safari" => "/Safari(.*)/i"
|
||||||
|
];
|
||||||
|
//TODO find a way to return also the version of the browser
|
||||||
|
foreach ($browsers as $browser => $regex) {
|
||||||
|
if (preg_match($regex, $this->agent)) {
|
||||||
|
return $browser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Operating system information
|
||||||
|
*
|
||||||
|
* @return string os information
|
||||||
|
*/
|
||||||
|
public function getOs()
|
||||||
|
{
|
||||||
|
// get string before the first appearance of ')'
|
||||||
|
$device = strstr($this->agent, ')', true);
|
||||||
|
if (! $device) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
// return string after the first appearance of '('
|
||||||
|
return substr($device, strpos($device, '(') + 1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -103,6 +103,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="submit"]:focus {
|
||||||
|
outline: 3px solid fade(@icinga-blue, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
.form-controls {
|
.form-controls {
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
|
@ -134,6 +138,21 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control-group:nth-child(3) {
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 1.25em;
|
||||||
|
|
||||||
|
> .control-label-group {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .toggle-switch {
|
||||||
|
float: left;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#social {
|
#social {
|
||||||
|
|
Loading…
Reference in New Issue