<?php /* ** Copyright (C) 2001-2025 Zabbix SIA ** ** This program is free software: you can redistribute it and/or modify it under the terms of ** the GNU Affero General Public License as published by the Free Software Foundation, version 3. ** ** 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 Affero General Public License for more details. ** ** You should have received a copy of the GNU Affero General Public License along with this program. ** If not, see <https://www.gnu.org/licenses/>. **/ require_once dirname(__FILE__) . '/../include/CAPITest.php'; /** * @backup mfa, config, usrgrp, users, mfa_totp_secret * * @onBefore prepareTestData * * @onAfter cleanTestData */ class testMfa extends CAPITest { public static $data = [ 'mfaids' => [], 'mfas' => [ 'TOTP test case 1' => [ 'type' => MFA_TYPE_TOTP, 'name' => 'TOTP test case 1', 'hash_function' => TOTP_HASH_SHA1, 'code_length' => TOTP_CODE_LENGTH_8 ], 'DUO test case 1' => [ 'type' => MFA_TYPE_DUO, 'name' => 'DUO test case 1', 'api_hostname' => 'api-999a9a99.duosecurity.com', 'clientid' => 'AAA58NOODEGUA6ST7AAA', 'client_secret' => '1AaAaAaaAaA7OoB4AaQfV547ARiqOqRNxP32Cult' ], 'DUO test case 2' => [ 'type' => MFA_TYPE_DUO, 'name' => 'DUO test case 2', 'api_hostname' => 'api-999a9a99.duosecurity.com', 'clientid' => 'AAA58NOODEGUA6ST7AAA', 'client_secret' => '1AaAaAaaAaA7OoB4AaQfV547ARiqOqRNxP32Cult' ] ], 'usrgrpids' => [], 'userids' => [] ]; public function prepareTestData() { $mfaids = CDataHelper::call('mfa.create', array_values(self::$data['mfas'])); $this->assertArrayHasKey('mfaids', $mfaids); self::$data['mfaids'] = array_combine(array_keys(self::$data['mfas']), $mfaids['mfaids']); CDataHelper::call('authentication.update', [ 'mfaid' => self::$data['mfaids']['DUO test case 1'], 'mfa_status' => MFA_ENABLED ]); $usrgrpids = CDataHelper::call('usergroup.create', [ 'name' => 'User group with MFA', 'mfa_status' => GROUP_MFA_ENABLED, 'mfaid' => self::$data['mfaids']['DUO test case 2'] ]); $this->assertArrayHasKey('usrgrpids', $usrgrpids); self::$data['usrgrpids'] = array_combine(['User group with MFA'], $usrgrpids['usrgrpids']); $userids = CDataHelper::call('user.create', [ 'username' => 'User with MFA TOTP method', 'roleid' => 1, 'passwd' => 'Z@bb1x1234', 'usrgrps' => [ ['usrgrpid' => 7] ] ]); $this->assertArrayHasKey('userids', $userids); self::$data['userids'] = array_combine(['User with MFA TOTP method'], $userids['userids']); DB::insert('mfa_totp_secret', [[ 'mfaid' => self::$data['mfaids']['TOTP test case 1'], 'userid' => self::$data['userids']['User with MFA TOTP method'], 'totp_secret' => '123asdf123asdf13asdf123asdf123as', 'status' => TOTP_SECRET_CONFIRMED ]]); } public function resolveids($mfas) { $resolved_data = $mfas; foreach ($mfas as $key => $mfa) { if ($key === 'mfaids') { foreach ($mfa as $index => $mfaid) { if (array_key_exists($mfaid, self::$data['mfaids'])) { $resolved_data[$key][$index] = self::$data['mfaids'][$mfaid]; } } } else { if (array_key_exists($mfa['mfaid'], self::$data['mfaids'])){ $resolved_data[$key]['mfaid'] = self::$data['mfaids'][$mfa['mfaid']]; } } } return $resolved_data; } public static function createValidDataProvider() { return [ 'Create TOTP MFA methods' => [ 'mfas' => [ ['type' => MFA_TYPE_TOTP, 'name' => 'TOTP 1', 'hash_function' => TOTP_HASH_SHA1, 'code_length' => TOTP_CODE_LENGTH_6 ], ['type' => MFA_TYPE_TOTP, 'name' => 'TOTP 2', 'hash_function' => TOTP_HASH_SHA256, 'code_length' => TOTP_CODE_LENGTH_8 ], ['type' => MFA_TYPE_TOTP, 'name' => 'TOTP 3', 'hash_function' => TOTP_HASH_SHA512, 'code_length' => TOTP_CODE_LENGTH_8 ] ], 'expected_error' => null ], 'Create DUO MFA method' => [ 'mfas' => [ ['type' => MFA_TYPE_DUO, 'name' => 'DUO 1', 'api_hostname' => 'api-999a9a99.duosecurity.com', 'clientid' => 'AAA58NOODEGUA6ST7AAA', 'client_secret' => '1AaAaAaaAaA7OoB4AaQfV547ARiqOqRNxP32Cult' ], ['type' => MFA_TYPE_DUO, 'name' => 'DUO 2', 'api_hostname' => 'api-888a8a88.duosecurity.com', 'clientid' => 'BBB58NOODEGUA6ST7BBB', 'client_secret' => '1BbBbBbbBbB7OoB4AaQfV547ARiqOqRNxP32Cult' ] ], 'expected_error' => null ] ]; } public static function createInvalidDataProvider() { return [ 'Duplicate names in one request' => [ 'mfas' => [ ['type' => MFA_TYPE_TOTP, 'name' => 'TOTP 1', 'hash_function' => TOTP_HASH_SHA1, 'code_length' => TOTP_CODE_LENGTH_6 ], ['type' => MFA_TYPE_TOTP, 'name' => 'TOTP 1', 'hash_function' => TOTP_HASH_SHA1, 'code_length' => TOTP_CODE_LENGTH_6 ] ], 'expected_error' => 'Invalid parameter "/2": value (name)=(TOTP 1) already exists.' ], 'MFA with already existing name' => [ 'mfas' => [ ['type' => MFA_TYPE_TOTP, 'name' => 'TOTP 1', 'hash_function' => TOTP_HASH_SHA1, 'code_length' => TOTP_CODE_LENGTH_6 ] ], 'expected_error' => 'MFA method "TOTP 1" already exists.' ], 'Missing MFA name' => [ 'mfas' => [ ['type' => MFA_TYPE_TOTP, 'hash_function' => TOTP_HASH_SHA1, 'code_length' => TOTP_CODE_LENGTH_6], ['type' => MFA_TYPE_DUO, 'api_hostname' => 'api-999a9a99.duosecurity.com', 'clientid' => 'AAA58NOODEGUA6ST7AAA', 'client_secret' => '1AaAaAaaAaA7OoB4AaQfV547ARiqOqRNxP32Cult' ] ], 'expected_error' => 'Invalid parameter "/1": the parameter "name" is missing.' ], 'Missing MFA type' => [ 'mfas' => [ ['name' => 'TOTP 3'] ], 'expected_error' => 'Invalid parameter "/1": the parameter "type" is missing.' ], 'Missing MFA DUO api_hostname' => [ 'mfas' => [ ['type' => MFA_TYPE_DUO, 'name' => 'DUO 4', 'clientid' => 'AAA58NOODEGUA6ST7AAA', 'client_secret' => '1AaAaAaaAaA7OoB4AaQfV547ARiqOqRNxP32Cult' ] ], 'expected_error' => 'Invalid parameter "/1": the parameter "api_hostname" is missing.' ], 'Missing MFA DUO clientid' => [ 'mfas' => [ ['type' => MFA_TYPE_DUO, 'name' => 'DUO 4', 'api_hostname' => 'api-999a9a99.duosecurity.com', 'client_secret' => '1AaAaAaaAaA7OoB4AaQfV547ARiqOqRNxP32Cult' ] ], 'expected_error' => 'Invalid parameter "/1": the parameter "clientid" is missing.' ], 'Missing MFA DUO client_secret' => [ 'mfas' => [ ['type' => MFA_TYPE_DUO, 'name' => 'DUO 4', 'api_hostname' => 'api-999a9a99.duosecurity.com', 'clientid' => 'AAA58NOODEGUA6ST7AAA' ] ], 'expected_error' => 'Invalid parameter "/1": the parameter "client_secret" is missing.' ], 'Non-default parameter api_hostname with TOTP method' => [ 'mfas' => [ ['type' => MFA_TYPE_TOTP, 'name' => 'TOTP 4', 'hash_function' => TOTP_HASH_SHA1, 'code_length' => TOTP_CODE_LENGTH_6, 'api_hostname' => 'api-999a9a99.duosecurity.com' ] ], 'expected_error' => 'Invalid parameter "/1/api_hostname": value must be empty.' ], 'Non-default parameter has_function with DUO method' => [ 'mfas' => [ ['type' => MFA_TYPE_DUO, 'name' => 'DUO 4', 'api_hostname' => 'api-999a9a99.duosecurity.com', 'clientid' => 'AAA58NOODEGUA6ST7AAA', 'client_secret' => '1AaAaAaaAaA7OoB4AaQfV547ARiqOqRNxP32Cult', 'hash_function' => TOTP_HASH_SHA256 ] ], 'expected_error' => 'Invalid parameter "/1/hash_function": value must be 1.' ] ]; } /** * @dataProvider createValidDataProvider * @dataProvider createInvalidDataProvider */ public function testCreate($mfas, $expected_error) { $response = $this->call('mfa.create', $mfas, $expected_error); if ($expected_error === null) { self::$data['mfaids'] += array_combine(array_column($mfas, 'name'), $response['result']['mfaids'] ); } } public static function updateValidDataProvider(): array { return [ 'Update TOTP method name' => [ 'mfas' => [ ['mfaid' => 'TOTP test case 1', 'name' => 'NEW TOTP test case 1'] ], 'expected_error' => null ], 'Update TOTP method hash_function' => [ 'mfas' => [ ['mfaid' => 'TOTP test case 1', 'hash_function' => 2] ], 'expected_error' => null ], 'Update TOTP method code_length' => [ 'mfas' => [ ['mfaid' => 'TOTP test case 1', 'code_length' => 6] ], 'expected_error' => null ], 'Update Duo method name' => [ 'mfas' => [ ['mfaid' => 'DUO test case 1', 'name' => 'NEW DUO test case 1'] ], 'expected_error' => null ], 'Update Duo method api_hostname' => [ 'mfas' => [ ['mfaid' => 'DUO test case 1', 'api_hostname' => 'new.api.hostname'] ], 'expected_error' => null ], 'Update Duo method clientid' => [ 'mfas' => [ ['mfaid' => 'DUO test case 1', 'clientid' => 'clientidCLIENTIDclientid'] ], 'expected_error' => null ], 'Update Duo method client_secret' => [ 'mfas' => [ ['mfaid' => 'DUO test case 1', 'client_secret' => 'AAABBBCCCaaabbbccc'] ], 'expected_error' => null ], 'Update TOTP method to DUO method' => [ 'mfas' => [ [ 'mfaid' => 'TOTP test case 1', 'type' => MFA_TYPE_DUO, 'name' => 'DUO test case switch', 'api_hostname' => 'api-999a9a99.duosecurity.com', 'clientid' => 'AAA58NOODEGUA6ST7AAA', 'client_secret' => '1AaAaAaaAaA7OoB4AaQfV547ARiqOqRNxP32Cult' ] ], 'expected_error' => null ], 'Update DUO method back to TOTP method' => [ 'mfas' => [ [ 'mfaid' => 'TOTP test case 1', 'type' => MFA_TYPE_TOTP, 'name' => 'TOTP test case 1', 'hash_function' => TOTP_HASH_SHA1, 'code_length' => TOTP_CODE_LENGTH_8 ] ], 'expected_error' => null ] ]; } public static function updateInvalidDataProvider(): array { return [ 'Update duplicate name' => [ 'mfas' => [ ['mfaid' => 'TOTP test case 1', 'name' => 'NEW DUO test case 1'] ], 'expected_error' => 'MFA method "NEW DUO test case 1" already exists.' ], 'Update duplicate names - cross name update' => [ 'mfas' => [ ['mfaid' => 'TOTP test case 1', 'name' => 'NEW DUO test case 1'], ['mfaid' => 'DUO test case 1', 'name' => 'NEW TOTP test case 1'] ], 'expected_error' => 'MFA method "NEW DUO test case 1" already exists.' ], 'Update non-existing MFA' => [ 'mfas' => [ ['mfaid' => 1234, 'name' => 'TOTP'] ], 'expected_error' => 'No permissions to referred object or it does not exist!' ], 'Update DUO specific field to TOTP method' => [ 'mfas' => [ ['mfaid' => 'TOTP test case 1', 'api_hostname' => 'host.name'] ], 'expected_error' => 'Invalid parameter "/1/api_hostname": value must be empty.' ], 'Update TOTP specific field to DUO method' => [ 'mfas' => [ ['mfaid' => 'DUO test case 1', 'code_length' => TOTP_CODE_LENGTH_8] ], 'expected_error' => 'Invalid parameter "/1/code_length": value must be 6.' ], 'Update TOTP method with invalid hash_function' => [ 'mfas' => [ ['mfaid' => 'TOTP test case 1', 'hash_function' => 99] ], 'expected_error' => 'Invalid parameter "/1/hash_function": value must be one of ' . implode(', ', [TOTP_HASH_SHA1, TOTP_HASH_SHA256, TOTP_HASH_SHA512]) . "." ], 'Update TOTP method with invalid code_length' => [ 'mfas' => [ ['mfaid' => 'TOTP test case 1', 'code_length' => 10] ], 'expected_error' => 'Invalid parameter "/1/code_length": value must be one of ' . implode(', ', [TOTP_CODE_LENGTH_6, TOTP_CODE_LENGTH_8]) . "." ] ]; } /** * @dataProvider updateValidDataProvider * @dataProvider updateInvalidDataProvider */ public function testUpdate(array $mfas, $expected_error) { $mfas = $this->resolveids($mfas); $this->call('mfa.update', $mfas, $expected_error); } public static function deleteValidDataProvider(): array { return [ 'Test delete MFA method' => [ 'mfas' => [ 'mfaids' => ['TOTP test case 1'] ], 'expected_error' => null ] ]; } public static function deleteInvalidDataProvider(): array { return [ 'Test delete MFA with user group' => [ 'mfas' => [ 'mfaids' => ['DUO test case 2'] ], 'expected_error' => 'Cannot delete MFA method "DUO test case 2", because it is used by user group "User group with MFA".' ], 'Test delete id does not exists' => [ 'mfas' => [ 'mfaids' => [1234] ], 'expected_error' => 'No permissions to referred object or it does not exist!' ] ]; } /** * @dataProvider deleteValidDataProvider * @dataProvider deleteInvalidDataProvider */ public function testDelete(array $mfas, $expected_error): void { $mfas = $this->resolveids($mfas); $this->assertNotEmpty($mfas, 'No Mfas to test delete'); $this->call('mfa.delete', $mfas['mfaids'], $expected_error); if ($expected_error === null) { $mfa_totp_secrets = DB::select('mfa_totp_secret', [ 'output' => ['userid'], 'filter' => ['userid' => self::$data['userids']['User with MFA TOTP method']] ]); $this->assertEmpty($mfa_totp_secrets, 'The entry in mfa_totp_secret has not been deleted.'); self::$data['mfaids'] = array_diff(self::$data['mfaids'], $mfas['mfaids']); } } public static function deleteLastDefaultDataProvider(): array { return [ 'Test delete default MFA method' => [ 'mfas' => [ 'mfaids' => ['DUO test case 1'] ], 'expected_error' => 'Cannot delete default MFA method.' ] ]; } /** * @dataProvider deleteLastDefaultDataProvider */ public function testDeleteLastDefaultMfa(array $mfas, $expected_error): void { $mfas = $this->resolveids($mfas); CDataHelper::call('usergroup.delete', array_values(self::$data['usrgrpids'])); // Remove other MFA methods to test deletion of the final default MFA method. DBexecute( 'DELETE FROM mfa'. ' WHERE '.dbConditionId('mfaid', $mfas['mfaids'], true) ); $this->call('mfa.delete', $mfas['mfaids'], $expected_error); } /** * Remove data created for tests. */ public static function cleanTestData(): void { CDataHelper::call('authentication.update', ['mfaid' => 0, 'mfa_status' => MFA_DISABLED]); CDataHelper::call('usergroup.delete', array_values(self::$data['usrgrpids'])); CDataHelper::call('mfa.delete', array_values(self::$data['mfaids'])); CDataHelper::call('user.delete', array_values(self::$data['userids'])); } }