Skip to content

Commit

Permalink
Add deny login action to authorization rules
Browse files Browse the repository at this point in the history
  • Loading branch information
cconard96 authored and trasher committed Jan 12, 2024
1 parent f81ac57 commit 6cd407a
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 43 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The present file will list all changes made to the project; according to the
- Log viewer for logs in `files/_log` directory.
- Custom palette/theme support (uses `files/_themes` directory by default).
- Two-Factor Authentication (2FA) support via Time-based One-time Password (TOTP).
- `Deny login` authorization rule action to deny login for a user, but not prevent the import/existence of the user in GLPI.

### Changed
- ITIL Objects can now be linked to any other ITIL Objects similar to the previous Ticket/Ticket links.
Expand Down
100 changes: 58 additions & 42 deletions src/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ class Auth extends CommonGLPI
public $user_present = 0;
/** @var int Indicates if the user password expired */
public $password_expired = false;
/** @var bool Indicates the login was valid by explicitly denied by a rule */
public $denied_by_rule = false;

/**
* Indicated if user was found in the directory.
Expand Down Expand Up @@ -1031,42 +1033,48 @@ public function login($login_name, $login_password, $noauto = false, $remember_m
// if not, we update him.

if ($mfa_pre_auth || $this->validateLogin($login_name, $login_password, $noauto, $login_auth)) {
// Check MFA
$totp = new TOTPManager();
$web_access = !isAPI() && !isCommandLine();

// In some cases, the session is restored from a remember me cookie.
// This results in a redirect loop because there is no mfa_pre_auth session variable, but the login is revalidated when the username and password passed here are empty.
// In this case, since the user is technically still logged in, we can just say the login is valid and not process any MFA stuff.
if ($web_access && $this->auth_type !== self::COOKIE) {
$enforcement = $totp->get2FAEnforcement($this->user->fields['id']);
if ($totp->is2FAEnabled($this->user->fields['id'])) {
if (!isset($mfa_params['totp_code']) && !isset($mfa_params['backup_code'])) {
// Need to remember that this user entered the correct username/password, and then ask for the TOTP token
$_SESSION['mfa_pre_auth'] = [
'user' => $this->user->fields,
'auth_type' => $this->auth_type,
'extauth' => $this->extauth,
'remember_me' => $remember_me,
];
Html::redirect($CFG_GLPI["root_doc"] . '/?mfa=1');
} else if (isset($mfa_params['totp_code']) && !$totp->verifyCodeForUser($mfa_params['totp_code'], $this->user->fields['id'])) {
$this->addToError(__('Invalid TOTP code'));
$this->auth_succeded = false;
} else if (isset($mfa_params['backup_code']) && !$totp->verifyBackupCodeForUser($mfa_params['backup_code'], $this->user->fields['id'])) {
$this->addToError(__('Invalid backup code'));
$this->auth_succeded = false;
}
} else if ($enforcement !== TOTPManager::ENFORCEMENT_OPTIONAL) {
if ($enforcement === TOTPManager::ENFORCEMENT_MANDATORY || !isset($_REQUEST['skip_mfa'])) {
// If MFA is mandatory the user has not already skipped MFA while in a grace period for this login, then we need to ask for it now
$_SESSION['mfa_pre_auth'] = [
'user' => $this->user->fields,
'auth_type' => $this->auth_type,
'extauth' => $this->extauth,
'remember_me' => $remember_me,
];
Html::redirect($CFG_GLPI["root_doc"] . '/?mfa_setup=1');
if (isset($this->user->fields['_deny_login'])) {
$this->addToError(__('User not authorized to connect in GLPI'));
$this->auth_succeded = false;
$this->denied_by_rule = true;
} else {
// Check MFA
$totp = new TOTPManager();
$web_access = !isAPI() && !isCommandLine();

// In some cases, the session is restored from a remember me cookie.
// This results in a redirect loop because there is no mfa_pre_auth session variable, but the login is revalidated when the username and password passed here are empty.
// In this case, since the user is technically still logged in, we can just say the login is valid and not process any MFA stuff.
if ($web_access && $this->auth_type !== self::COOKIE) {
$enforcement = $totp->get2FAEnforcement($this->user->fields['id']);
if ($totp->is2FAEnabled($this->user->fields['id'])) {
if (!isset($mfa_params['totp_code']) && !isset($mfa_params['backup_code'])) {
// Need to remember that this user entered the correct username/password, and then ask for the TOTP token
$_SESSION['mfa_pre_auth'] = [
'user' => $this->user->fields,
'auth_type' => $this->auth_type,
'extauth' => $this->extauth,
'remember_me' => $remember_me,
];
Html::redirect($CFG_GLPI["root_doc"] . '/?mfa=1');
} else if (isset($mfa_params['totp_code']) && !$totp->verifyCodeForUser($mfa_params['totp_code'], $this->user->fields['id'])) {
$this->addToError(__('Invalid TOTP code'));
$this->auth_succeded = false;
} else if (isset($mfa_params['backup_code']) && !$totp->verifyBackupCodeForUser($mfa_params['backup_code'], $this->user->fields['id'])) {
$this->addToError(__('Invalid backup code'));
$this->auth_succeded = false;
}
} else if ($enforcement !== TOTPManager::ENFORCEMENT_OPTIONAL) {
if ($enforcement === TOTPManager::ENFORCEMENT_MANDATORY || !isset($_REQUEST['skip_mfa'])) {
// If MFA is mandatory the user has not already skipped MFA while in a grace period for this login, then we need to ask for it now
$_SESSION['mfa_pre_auth'] = [
'user' => $this->user->fields,
'auth_type' => $this->auth_type,
'extauth' => $this->extauth,
'remember_me' => $remember_me,
];
Html::redirect($CFG_GLPI["root_doc"] . '/?mfa_setup=1');
}
}
}
}
Expand Down Expand Up @@ -1135,12 +1143,20 @@ public function login($login_name, $login_password, $noauto = false, $remember_m
"Connection failed for " . $login_name . " ($ip)"
);
} else {
//TRANS: %1$s is the login of the user and %2$s its IP address
Event::log(0, "system", 3, "login", sprintf(
__('Failed login for %1$s from IP %2$s'),
$login_name,
$ip
));
if ($this->denied_by_rule) {
Event::log(0, "system", 3, "login", sprintf(
__('Login for %1$s denied by authorization rules from IP %2$s'),
$login_name,
$ip
));
} else {
//TRANS: %1$s is the login of the user and %2$s its IP address
Event::log(0, "system", 3, "login", sprintf(
__('Failed login for %1$s from IP %2$s'),
$login_name,
$ip
));
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/RuleRight.php
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,10 @@ public function getActions()
$actions['timezone']['name'] = __('Timezone');
$actions['timezone']['type'] = 'timezone';

$actions['_deny_login']['name'] = __('Deny login');
$actions['_deny_login']['type'] = 'yesonly';
$actions['_deny_login']['table'] = '';

return $actions;
}

Expand Down
41 changes: 40 additions & 1 deletion tests/units/RuleRight.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function testGetActions()
{
$rule = new \RuleRight();
$actions = $rule->getActions();
$this->array($actions)->size->isGreaterThan(11);
$this->array($actions)->size->isGreaterThan(12);
}

public function testDefaultRuleExists()
Expand Down Expand Up @@ -221,4 +221,43 @@ public function testLocalAccountNoRules()
// Clean session
$this->login();
}

public function testDenyLogin()
{
$rule = new \RuleRight();
$this->integer($rules_id = $rule->add([
'sub_type' => 'RuleRight',
'name' => 'deny login',
'match' => 'AND',
'is_active' => 1,
'entities_id' => 0,
'is_recursive' => 1,
]));
$criteria = new \RuleCriteria();
$this->integer($criteria->add([
'rules_id' => $rules_id,
'criteria' => 'LOGIN',
'condition' => \Rule::PATTERN_IS,
'pattern' => TU_USER,
]))->isGreaterThan(0);

$actions = new \RuleAction();
$this->integer($actions->add([
'rules_id' => $rules_id,
'action_type' => 'assign',
'field' => '_deny_login',
'value' => 1,
]))->isGreaterThan(0);

$this->login(TU_USER, TU_PASS, true, false);
$events = getAllDataFromTable('glpi_events', [
'service' => 'login',
'type' => 'system',
'items_id' => 0,
]);
$username = TU_USER;
$this->array(array_filter($events, static function ($event) use ($username) {
return str_starts_with($event['message'], "Login for {$username} denied by authorization rules from IP ");
}))->size->isIdenticalTo(1);
}
}

0 comments on commit 6cd407a

Please sign in to comment.