<?php
/*
** Zabbix
** Copyright (C) 2001-2022 Zabbix SIA
**
** 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 2 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, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/

require_once dirname(__FILE__).'/../include/CWebTest.php';
require_once dirname(__FILE__).'/behaviors/CMessageBehavior.php';

/**
 * @backup config
 */
class testFormAdministrationAuthenticationSaml extends CWebTest {

	protected function onBeforeTestSuite() {
		if (!defined('PHPUNIT_SAML_TESTS_ENABLED') || !PHPUNIT_SAML_TESTS_ENABLED) {
			self::markTestSuiteSkipped();
		}
	}

	/**
	 * Attach MessageBehavior to the test.
	 *
	 * @return array
	 */
	public function getBehaviors() {
		return [
			'class' => CMessageBehavior::class
		];
	}

	public function getSamlData() {
		return [
			// Missing IdP entity ID
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'SSO service URL' => 'SSO',
						'Username attribute' => 'UA',
						'SP entity ID' => 'SP'
					],
					'error' => 'Incorrect value for field "saml_idp_entityid": cannot be empty.'
				]
			],
			// Missing SSO service URL
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'IdP entity ID' => 'IdP',
						'Username attribute' => 'UA',
						'SP entity ID' => 'SP'
					],
					'error' => 'Incorrect value for field "saml_sso_url": cannot be empty.'
				]
			],
			// Missing Username attribute
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'SSO service URL' => 'SSO',
						'IdP entity ID' => 'IdP',
						'SP entity ID' => 'SP'
					],
					'error' => 'Incorrect value for field "saml_username_attribute": cannot be empty.'
				]
			],
			// Missing SP entity ID
			[
				[
					'expected' => TEST_BAD,
					'fields' => [
						'SSO service URL' => 'SSO',
						'IdP entity ID' => 'IdP',
						'Username attribute' => 'UA'
					],
					'error' => 'Incorrect value for field "saml_sp_entityid": cannot be empty.'
				]
			],
			// Configure SAML with only
			[
				[
					'fields' => [
						'IdP entity ID' => 'IdP',
						'SSO service URL' => 'SSO',
						'Username attribute' => 'UA',
						'SP entity ID' => 'SP'
					]
				]
			],
			// Various UTF-8 characters in SAML settings fields
			[
				[
					'fields' => [
						'IdP entity ID' => '!@#$%^&*()_+-=[]{};:"|,./<>?Ž©µÆ',
						'SSO service URL' => '!@#$%^&*()_+-=[]{};:"|,./<>?Ž©µÆ',
						'Username attribute' => '!@#$%^&*()_+-=[]{};:"|,./<>?Ž©µÆ',
						'SP entity ID' => '!@#$%^&*()_+-=[]{};:"|,./<>?Ž©µÆ',
						'SLO service URL' => '!@#$%^&*()_+-=[]{};:"|,./<>?Ž©µÆ',
						'SP name ID format' => '!@#$%^&*()_+-=[]{};:"|,./<>?Ž©µÆ'
					]
				]
			],
			// SAML settings with leading and trailing spaces
			[
				[
					'fields' => [
						'IdP entity ID' => '   leading.trailing   ',
						'SSO service URL' => '   leading.trailing   ',
						'Username attribute' => '   leading.trailing   ',
						'SP entity ID' => '   leading.trailing   ',
						'SLO service URL' => '   leading.trailing   ',
						'SP name ID format' => '   leading.trailing   '
					],
					'trim' => true
				]
			],
			// Configure SAML with all possible parameters
			[
				[
					'fields' => [
						'IdP entity ID' => 'IdP_saml_zabbix.com',
						'SSO service URL' => 'SSO_saml_zabbix.com',
						'SLO service URL' => 'SLO_saml_zabbix.com',
						'Username attribute' => 'Username attribute',
						'SP entity ID' => 'SP entity ID',
						'SP name ID format' => 'SP name ID format',
						'Sign' => ['Messages', 'Assertions', 'AuthN requests', 'Logout requests', 'Logout responses'],
						'Encrypt' => ['Name ID', 'Assertions'],
						'Case sensitive login' => true
					],
					'check_disabled' => true,
					'db_check' => [
						'saml_auth_enabled' => '1',
						'saml_idp_entityid' => 'IdP_saml_zabbix.com',
						'saml_sso_url' => 'SSO_saml_zabbix.com',
						'saml_slo_url' => 'SLO_saml_zabbix.com',
						'saml_username_attribute' => 'Username attribute',
						'saml_sp_entityid' => 'SP entity ID',
						'saml_nameid_format' => 'SP name ID format',
						'saml_sign_messages' => '1',
						'saml_sign_assertions' => '1',
						'saml_sign_authn_requests' => '1',
						'saml_sign_logout_requests' => '1',
						'saml_sign_logout_responses' => '1',
						'saml_encrypt_nameid' => '1',
						'saml_encrypt_assertions' => '1',
						'saml_case_sensitive' => '1'
					]
				]
			]
		];
	}

	/**
	 * @backup config
	 * @dataProvider getSamlData
	 */
	public function testFormAdministrationAuthenticationSaml_Configure($data) {
		$old_hash = CDBHelper::getHash('SELECT * FROM config');
		$this->page->login()->open('zabbix.php?action=authentication.edit');

		// Check that SAML settings are disabled by default and configure SAML authentication.
		$this->configureSamlAuthentication($data['fields'], true);

		// Check SAML settings update messages and, in case of successful update, check that field values were saved.
		if (CTestArrayHelper::get($data, 'expected', TEST_GOOD) === TEST_BAD) {
			$this->assertMessage(TEST_BAD, $data['error']);
			$this->assertEquals($old_hash, CDBHelper::getHash('SELECT * FROM config'));
		}
		else {
			$this->assertMessage(TEST_GOOD, 'Authentication settings updated');
			$form = $this->query('name:form_auth')->asForm()->one();
			$form->selectTab('SAML settings');
			$this->assertTrue($form->getField('Enable SAML authentication')->isChecked());
			// Trim trailing and leading spaces in expected values before comparison.
			if (CTestArrayHelper::get($data, 'trim', false)) {
				$data['fields'] = array_map('trim', $data['fields']);
			}
			$form->checkValue($data['fields']);
			if (array_key_exists('db_check', $data)) {
				$sql = 'SELECT saml_auth_enabled, saml_idp_entityid, saml_sso_url, saml_slo_url, saml_username_attribute,'.
						' saml_sp_entityid, saml_nameid_format, saml_sign_messages, saml_sign_assertions,'.
						' saml_sign_authn_requests, saml_sign_logout_requests, saml_sign_logout_responses,'.
						' saml_encrypt_nameid, saml_encrypt_assertions, saml_case_sensitive'.
						' FROM config';
				$result = CDBHelper::getRow($sql);
				$this->assertEquals($data['db_check'], $result);
			}
		}
	}

	public function testFormAdministrationAuthenticationSaml_CheckStatusChange() {
		$settings = [
			'IdP entity ID' => 'IdP',
			'SSO service URL' => 'SSO',
			'Username attribute' => 'UA',
			'SP entity ID' => 'SP'
		];
		$this->page->login()->open('zabbix.php?action=authentication.edit');

		$this->configureSamlAuthentication($settings);
		// Logout and check that SAML authentication was enabled.
		$this->page->logout();
		$this->page->open('index.php')->waitUntilReady();
		$link = $this->query('link:Sign in with Single Sign-On (SAML)')->one()->waitUntilClickable();
		$this->assertStringContainsString('index_sso.php', $link->getAttribute('href'));
		// Login and disable SAML authentication.
		$this->page->login()->open('zabbix.php?action=authentication.edit');
		$form = $this->query('name:form_auth')->asForm()->one();
		$form->selectTab('SAML settings');
		$form->getField('Enable SAML authentication')->uncheck();
		$form->submit();
		// Logout and check that SAML authentication was disabled.
		$this->page->logout();
		$this->page->open('index.php')->waitUntilReady();
		$this->assertTrue($this->query('link:Sign in with Single Sign-On (SAML)')->count() === 0, 'Link must not exist.');
	}

	public function getAuthenticationDetails() {
		return [
			// Login as zabbix super admin - case insensitive login
			[
				[
					'username' => 'admin'
				]
			],
			// Login as zabbix super admin - case sensitive login
			[
				[
					'username' => 'Admin',
					'custom_settings' => [
						'Case sensitive login' => true
					]
				]
			],
			// Login as zabbix user
			[
				[
					'username' => 'user-zabbix'
				]
			],
			// Login as zabbix admin with custom url after login
			[
				[
					'username' => 'admin-zabbix',
					// Remove the 'regular_login' flag when ZBX-17663 is fixed.
					'regular_login' => true,
					'header' => '100 busiest triggers'
				]
			],
			// Login as zabbix admin with pre-defined login url (has higher priority then the configured url after login).
			[
				[
					'username' => 'admin-zabbix',
					'url' => 'services.php',
					// Remove the 'regular_login' flag when ZBX-17663 is fixed.
					'regular_login' => true,
					'header' => 'Services'
				]
			],
			// Regular login
			[
				[
					'username' => 'Admin',
					'regular_login' => true
				]
			],
			// Incorrect IDP
			[
				[
					'expected' => TEST_BAD,
					'username' => 'admin',
					'custom_settings' => [
						'IdP entity ID' => 'metadata'
					],
					'error_title' => 'You are not logged in',
					'error_details' => 'Invalid issuer in the Assertion/Response'
				]
			],
			// UID exists only on IDP side.
			[
				[
					'expected' => TEST_BAD,
					'username' => 'Admin2',
					'error_title' => 'You are not logged in',
					'error_details' => 'Incorrect user name or password or account is temporarily blocked.'
				]
			],
			// Login as Admin - case sensitive login - negative test.
			[
				[
					'expected' => TEST_BAD,
					'username' => 'admin',
					'custom_settings' => [
						'Case sensitive login' => true
					],
					'error_title' => 'You are not logged in',
					'error_details' => 'Incorrect user name or password or account is temporarily blocked.'
				]
			]
		];
	}

	/**
	 * @ignoreBrowserErrors
	 * @backup config
	 * @dataProvider getAuthenticationDetails
	 */
	public function testFormAdministrationAuthenticationSaml_Authenticate($data) {
		$this->page->login()->open('zabbix.php?action=authentication.edit');
		$settings = [
			'IdP entity ID' => PHPUNIT_IDP_ENTITY_ID,
			'SSO service URL' => PHPUNIT_SSO_SERVICE_URL,
			'SLO service URL' => PHPUNIT_SLO_SERVICE_URL,
			'Username attribute' => PHPUNIT_USERNAME_ATTRIBUTE,
			'SP entity ID' => PHPUNIT_SP_ENTITY_ID,
			'Case sensitive login' => false
		];
		// Override particcular SAMl settings with values from data provider.
		if (array_key_exists('custom_settings', $data)) {
			foreach ($data['custom_settings'] as $key => $value) {
				$settings[$key] = $value;
			}
		}

		$this->configureSamlAuthentication($settings);

		// Logout and check that SAML authentication was enabled.
		$this->page->logout();
		// Login to a particcular url, if such is defined in data provider.
		if (array_key_exists('url', $data)) {
			$this->page->open($data['url'])->waitUntilReady();
			$this->query('button:Login')->one()->click();
			$this->page->waitUntilReady();
		}
		else {
			$this->page->open('index.php')->waitUntilReady();
		}
		// Login via regular Sing-in form or via SAML.
		if (CTestArrayHelper::get($data, 'regular_login', false)) {
			$this->query('id:name')->waitUntilVisible()->one()->fill($data['username']);
			$this->query('id:password')->one()->fill('zabbix');
			$this->query('button:Sign in')->one()->click();
		}
		else {
			$link = $this->query('link:Sign in with Single Sign-On (SAML)')->one()->waitUntilClickable()->click();
			$this->page->waitUntilReady();
			$this->query('id:username')->one()->waitUntilVisible()->fill($data['username']);
			$this->query('id:password')->one()->waitUntilVisible()->fill('zabbix');
			$this->query('button:Login')-> one()->click();
		}
		$this->page->waitUntilReady();
		// Check error message in case of negative test.
		if (CTestArrayHelper::get($data, 'expected', TEST_GOOD) === TEST_BAD) {
			$this->assertMessage(TEST_BAD, $data['error_title'], $data['error_details']);
			return;
		}
		// Check the header of the page that was displayed to the user after login.
		$header = CTestArrayHelper::get($data, 'header', 'Global view');
		$this->assertEquals($header, $this->query('tag:h1')->one()->getText());

		// Make sure that it is possible to log out.
		$this->query('link:Sign out')->one()->click();
		$this->page->waitUntilReady();
		$this->assertStringContainsString('index.php', $this->page->getCurrentUrl());
	}

	/**
	 * Function checks that SAML settings are disabled by default, if the corresponding flag is specified, enables and
	 * fills SAML settings, and submits the form.
	 */
	private function configureSamlAuthentication($fields, $check_enabled = false) {
		$form = $this->query('name:form_auth')->asForm()->one();
		$form->selectTab('SAML settings');
		// Check that SAML settings are disabled by default.
		if ($check_enabled === true) {
			foreach($fields as $name => $value){
				$this->assertFalse($form->getField($name)->isEnabled());
			}
		}
		$form->getField('Enable SAML authentication')->check();
		$form->fill($fields);
		$form->submit();
		$this->page->waitUntilReady();
	}
}