<?php
/*
** Zabbix
** Copyright (C) 2001-2024 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.
**/


class CControllerAuthenticationUpdate extends CController {

	/**
	 * @var CControllerResponseRedirect
	 */
	private $response;

	protected function init() {
		$this->response = new CControllerResponseRedirect((new CUrl('zabbix.php'))
			->setArgument('action', 'authentication.edit')
			->getUrl()
		);
	}

	protected function checkInput() {
		$fields = [
			'form_refresh' =>				'int32',
			'ldap_test_user' =>				'string',
			'ldap_test_password' =>			'string',
			'ldap_test' =>					'in 1',
			'db_authentication_type' =>		'int32',
			'change_bind_password' =>		'in 0,1',
			'authentication_type' =>		'in '.ZBX_AUTH_INTERNAL.','.ZBX_AUTH_LDAP,
			'http_case_sensitive' =>		'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
			'ldap_case_sensitive' =>		'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
			'ldap_configured' =>			'in '.ZBX_AUTH_LDAP_DISABLED.','.ZBX_AUTH_LDAP_ENABLED,
			'ldap_host' =>					'db config.ldap_host',
			'ldap_port' =>					'int32',
			'ldap_base_dn' =>				'db config.ldap_base_dn',
			'ldap_bind_dn' =>				'db config.ldap_bind_dn',
			'ldap_search_attribute' =>		'db config.ldap_search_attribute',
			'ldap_bind_password' =>			'db config.ldap_bind_password',
			'http_auth_enabled' =>			'in '.ZBX_AUTH_HTTP_DISABLED.','.ZBX_AUTH_HTTP_ENABLED,
			'http_login_form' =>			'in '.ZBX_AUTH_FORM_ZABBIX.','.ZBX_AUTH_FORM_HTTP,
			'http_strip_domains' =>			'db config.http_strip_domains',
			'saml_auth_enabled' =>			'in '.ZBX_AUTH_SAML_DISABLED.','.ZBX_AUTH_SAML_ENABLED,
			'saml_idp_entityid' =>			'db config.saml_idp_entityid',
			'saml_sso_url' =>				'db config.saml_sso_url',
			'saml_slo_url' =>				'db config.saml_slo_url',
			'saml_username_attribute' =>	'db config.saml_username_attribute',
			'saml_sp_entityid' =>			'db config.saml_sp_entityid',
			'saml_nameid_format' =>			'db config.saml_nameid_format',
			'saml_sign_messages' =>			'in 0,1',
			'saml_sign_assertions' =>		'in 0,1',
			'saml_sign_authn_requests' =>	'in 0,1',
			'saml_sign_logout_requests' =>	'in 0,1',
			'saml_sign_logout_responses' =>	'in 0,1',
			'saml_encrypt_nameid' =>		'in 0,1',
			'saml_encrypt_assertions' =>	'in 0,1',
			'saml_case_sensitive' =>		'in '.ZBX_AUTH_CASE_INSENSITIVE.','.ZBX_AUTH_CASE_SENSITIVE,
			'passwd_min_length' =>			'int32',
			'passwd_check_rules' =>			'array'
		];

		$ret = $this->validateInput($fields);

		if (!$ret) {
			$this->response->setFormData($this->getInputAll());
			$this->setResponse($this->response);
		}

		return $ret;
	}

	/**
	 * Validate default authentication. Do not allow user to change default authentication to LDAP if LDAP is not
	 * configured.
	 *
	 * @return bool
	 */
	private function validateDefaultAuth() {
		$data = [
			'ldap_configured' => ZBX_AUTH_LDAP_DISABLED,
			'authentication_type' => ZBX_AUTH_INTERNAL
		];
		$this->getInputs($data, array_keys($data));

		$is_valid = ($data['authentication_type'] != ZBX_AUTH_LDAP
				|| $data['ldap_configured'] == ZBX_AUTH_LDAP_ENABLED);

		if (!$is_valid) {
			CMessageHelper::setErrorTitle(_s('Incorrect value for field "%1$s": %2$s.', 'authentication_type',
				_('LDAP is not configured')
			));
		}

		return $is_valid;
	}

	/**
	 * Validate LDAP settings.
	 *
	 * @return bool
	 */
	private function validateLdap() {
		$is_valid = true;
		$ldap_status = (new CFrontendSetup())->checkPhpLdapModule();
		$ldap_fields = ['ldap_host', 'ldap_port', 'ldap_base_dn', 'ldap_search_attribute', 'ldap_configured'];
		$ldap_auth_original = [
			'ldap_host' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_HOST),
			'ldap_port' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_PORT),
			'ldap_base_dn' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_BASE_DN),
			'ldap_search_attribute' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_SEARCH_ATTRIBUTE),
			'ldap_configured' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_CONFIGURED),
			'ldap_bind_dn' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_BIND_DN),
			'ldap_bind_password' => CAuthenticationHelper::get(CAuthenticationHelper::LDAP_BIND_PASSWORD)
		];
		$ldap_auth = $ldap_auth_original;
		$this->getInputs($ldap_auth, array_merge($ldap_fields, ['ldap_bind_dn', 'ldap_bind_password']));
		$ldap_auth_changed = array_diff_assoc($ldap_auth, $ldap_auth_original);

		if (!$ldap_auth_changed && !$this->hasInput('ldap_test')) {
			return $is_valid;
		}

		if ($this->getInput('ldap_bind_password', '') !== '') {
			$ldap_fields[] = 'ldap_bind_dn';
		}

		foreach ($ldap_fields as $field) {
			if (trim($ldap_auth[$field]) === '') {
				CMessageHelper::setErrorTitle(_s('Incorrect value for field "%1$s": %2$s.', $field, _('cannot be empty')));
				$is_valid = false;
				break;
			}
		}

		if ($is_valid
				&& ($ldap_auth['ldap_port'] < ZBX_MIN_PORT_NUMBER
					|| $ldap_auth['ldap_port'] > ZBX_MAX_PORT_NUMBER)) {
			CMessageHelper::setErrorTitle(_s(
				'Incorrect value "%1$s" for "%2$s" field: must be between %3$s and %4$s.', $this->getInput('ldap_port'),
				'ldap_port', ZBX_MIN_PORT_NUMBER, ZBX_MAX_PORT_NUMBER
			));
			$is_valid = false;
		}

		if ($ldap_status['result'] != CFrontendSetup::CHECK_OK) {
			CMessageHelper::setErrorTitle($ldap_status['error']);
			$is_valid = false;
		}
		elseif ($is_valid) {
			$ldap_validator = new CLdapAuthValidator([
				'conf' => [
					'host' => $ldap_auth['ldap_host'],
					'port' => $ldap_auth['ldap_port'],
					'base_dn' => $ldap_auth['ldap_base_dn'],
					'bind_dn' => $ldap_auth['ldap_bind_dn'],
					'bind_password' => $ldap_auth['ldap_bind_password'],
					'search_attribute' => $ldap_auth['ldap_search_attribute']
				],
				'detailed_errors' => true
			]);

			$login = $ldap_validator->validate([
				'username' => $this->getInput('ldap_test_user', CWebUser::$data['username']),
				'password' => $this->getInput('ldap_test_password', '')
			]);

			if (!$login) {
				CMessageHelper::setErrorTitle($ldap_validator->getError());
				$is_valid = false;
			}
		}

		return $is_valid;
	}

	/**
	 * Validate SAML authentication settings.
	 *
	 * @return bool
	 */
	private function validateSamlAuth() {
		$openssl_status = (new CFrontendSetup())->checkPhpOpenSsl();

		if ($openssl_status['result'] != CFrontendSetup::CHECK_OK) {
			CMessageHelper::setErrorTitle($openssl_status['error']);

			return false;
		}

		$saml_fields = ['saml_idp_entityid', 'saml_sso_url', 'saml_sp_entityid', 'saml_username_attribute'];
		$saml_auth = [
			'saml_idp_entityid' => CAuthenticationHelper::get(CAuthenticationHelper::SAML_IDP_ENTITYID),
			'saml_sso_url' => CAuthenticationHelper::get(CAuthenticationHelper::SAML_SSO_URL),
			'saml_sp_entityid' => CAuthenticationHelper::get(CAuthenticationHelper::SAML_SP_ENTITYID),
			'saml_username_attribute' => CAuthenticationHelper::get(CAuthenticationHelper::SAML_USERNAME_ATTRIBUTE)
		];
		$this->getInputs($saml_auth, $saml_fields);

		foreach ($saml_fields as $field) {
			if (trim($saml_auth[$field]) === '') {
				CMessageHelper::setErrorTitle(_s('Incorrect value for field "%1$s": %2$s.', $field, _('cannot be empty')));

				return false;
			}
		}

		return true;
	}

	/**
	 * Validate is user allowed to change configuration.
	 *
	 * @return bool
	 */
	protected function checkPermissions() {
		return $this->checkAccess(CRoleHelper::UI_ADMINISTRATION_AUTHENTICATION);
	}

	/**
	 * In case of error, convert array back to integer (string) so edit form does not fail.
	 *
	 * @return array
	 */
	public function getInputAll() {
		$input = parent::getInputAll();
		$rules = $input['passwd_check_rules'];
		$input['passwd_check_rules'] = 0x00;

		foreach ($rules as $rule) {
			$input['passwd_check_rules'] |= $rule;
		}

		// CNewValidator thinks int32 must be a string type integer.
		$input['passwd_check_rules'] = (string) $input['passwd_check_rules'];

		return $input;
	}

	protected function doAction() {
		$auth_valid = ($this->getInput('ldap_configured', '') == ZBX_AUTH_LDAP_ENABLED)
			? $this->validateLdap()
			: $this->validateDefaultAuth();

		if ($auth_valid && $this->getInput('saml_auth_enabled', ZBX_AUTH_SAML_DISABLED) == ZBX_AUTH_SAML_ENABLED) {
			if (!$this->validateSamlAuth()) {
				$auth_valid = false;
			}
		}

		if (!$auth_valid) {
			$this->response->setFormData($this->getInputAll());
			$this->setResponse($this->response);

			return;
		}

		// Only ZBX_AUTH_LDAP have 'Test' option.
		if ($this->hasInput('ldap_test')) {
			CMessageHelper::setSuccessTitle(_('LDAP login successful'));
			$this->response->setFormData($this->getInputAll());
			$this->setResponse($this->response);

			return;
		}

		$auth_params = [
			CAuthenticationHelper::AUTHENTICATION_TYPE,
			CAuthenticationHelper::HTTP_AUTH_ENABLED,
			CAuthenticationHelper::HTTP_LOGIN_FORM,
			CAuthenticationHelper::HTTP_STRIP_DOMAINS,
			CAuthenticationHelper::HTTP_CASE_SENSITIVE,
			CAuthenticationHelper::LDAP_CASE_SENSITIVE,
			CAuthenticationHelper::LDAP_CONFIGURED,
			CAuthenticationHelper::LDAP_HOST,
			CAuthenticationHelper::LDAP_PORT,
			CAuthenticationHelper::LDAP_BASE_DN,
			CAuthenticationHelper::LDAP_BIND_DN,
			CAuthenticationHelper::LDAP_SEARCH_ATTRIBUTE,
			CAuthenticationHelper::LDAP_BIND_PASSWORD,
			CAuthenticationHelper::SAML_AUTH_ENABLED,
			CAuthenticationHelper::SAML_IDP_ENTITYID,
			CAuthenticationHelper::SAML_SSO_URL,
			CAuthenticationHelper::SAML_SLO_URL,
			CAuthenticationHelper::SAML_USERNAME_ATTRIBUTE,
			CAuthenticationHelper::SAML_SP_ENTITYID,
			CAuthenticationHelper::SAML_NAMEID_FORMAT,
			CAuthenticationHelper::SAML_SIGN_MESSAGES,
			CAuthenticationHelper::SAML_SIGN_ASSERTIONS,
			CAuthenticationHelper::SAML_SIGN_AUTHN_REQUESTS,
			CAuthenticationHelper::SAML_SIGN_LOGOUT_REQUESTS,
			CAuthenticationHelper::SAML_SIGN_LOGOUT_RESPONSES,
			CAuthenticationHelper::SAML_ENCRYPT_NAMEID,
			CAuthenticationHelper::SAML_ENCRYPT_ASSERTIONS,
			CAuthenticationHelper::SAML_CASE_SENSITIVE,
			CAuthenticationHelper::PASSWD_MIN_LENGTH,
			CAuthenticationHelper::PASSWD_CHECK_RULES
		];
		$auth = [];
		foreach ($auth_params as $param) {
			$auth[$param] = CAuthenticationHelper::get($param);
		}

		$fields = [
			'authentication_type' => ZBX_AUTH_INTERNAL,
			'ldap_configured' => ZBX_AUTH_LDAP_DISABLED,
			'http_auth_enabled' => ZBX_AUTH_HTTP_DISABLED,
			'saml_auth_enabled' => ZBX_AUTH_SAML_DISABLED,
			'passwd_min_length' => DB::getDefault('config', 'passwd_min_length'),
			'passwd_check_rules' => DB::getDefault('config', 'passwd_check_rules')
		];

		if ($this->getInput('http_auth_enabled', ZBX_AUTH_HTTP_DISABLED) == ZBX_AUTH_HTTP_ENABLED) {
			$fields += [
				'http_case_sensitive' => 0,
				'http_login_form' => 0,
				'http_strip_domains' => ''
			];
		}

		if ($this->getInput('ldap_configured', ZBX_AUTH_LDAP_DISABLED) == ZBX_AUTH_LDAP_ENABLED) {
			$fields += [
				'ldap_host' => '',
				'ldap_port' => '',
				'ldap_base_dn' => '',
				'ldap_bind_dn' => '',
				'ldap_search_attribute' => '',
				'ldap_case_sensitive' => 0
			];

			if ($this->hasInput('ldap_bind_password')) {
				$fields['ldap_bind_password'] = '';
			}
			else {
				unset($auth[CAuthenticationHelper::LDAP_BIND_PASSWORD]);
			}
		}

		if ($this->getInput('saml_auth_enabled', ZBX_AUTH_SAML_DISABLED) == ZBX_AUTH_SAML_ENABLED) {
			$fields += [
				'saml_idp_entityid' => '',
				'saml_sso_url' => '',
				'saml_slo_url' => '',
				'saml_username_attribute' => '',
				'saml_sp_entityid' => '',
				'saml_nameid_format' => '',
				'saml_sign_messages' => 0,
				'saml_sign_assertions' => 0,
				'saml_sign_authn_requests' => 0,
				'saml_sign_logout_requests' => 0,
				'saml_sign_logout_responses' => 0,
				'saml_encrypt_nameid' => 0,
				'saml_encrypt_assertions' => 0,
				'saml_case_sensitive' => ZBX_AUTH_CASE_INSENSITIVE
			];
		}

		$data = $fields + $auth;
		$this->getInputs($data, array_keys($fields));

		$rules = $data['passwd_check_rules'];
		$data['passwd_check_rules'] = 0x00;

		foreach ($rules as $rule) {
			$data['passwd_check_rules'] |= $rule;
		}

		$data = array_diff_assoc($data, $auth);

		if (array_key_exists('ldap_bind_dn', $data) && trim($data['ldap_bind_dn']) === '') {
			$data['ldap_bind_password'] = '';
		}

		if ($data) {
			$result = API::Authentication()->update($data);

			if ($result) {
				if (array_key_exists('authentication_type', $data)) {
					$this->invalidateSessions();
				}

				CMessageHelper::setSuccessTitle(_('Authentication settings updated'));
			}
			else {
				$this->response->setFormData($this->getInputAll());
				CMessageHelper::setErrorTitle(_('Cannot update authentication'));
			}
		}
		else {
			CMessageHelper::setSuccessTitle(_('Authentication settings updated'));
		}

		$this->setResponse($this->response);
	}

	/**
	 * Mark all active GROUP_GUI_ACCESS_INTERNAL sessions, except current user sessions, as ZBX_SESSION_PASSIVE.
	 *
	 * @return bool
	 */
	private function invalidateSessions() {
		$result = true;
		$internal_auth_user_groups = API::UserGroup()->get([
			'output' => [],
			'filter' => [
				'gui_access' => GROUP_GUI_ACCESS_INTERNAL
			],
			'preservekeys' => true
		]);

		$internal_auth_users = API::User()->get([
			'output' => [],
			'usrgrpids' => array_keys($internal_auth_user_groups),
			'preservekeys' => true
		]);
		unset($internal_auth_users[CWebUser::$data['userid']]);

		if ($internal_auth_users) {
			DBstart();
			$result = DB::update('sessions', [
				'values' => ['status' => ZBX_SESSION_PASSIVE],
				'where' => ['userid' => array_keys($internal_auth_users)]
			]);
			$result = DBend($result);
		}

		return $result;
	}
}