['method' => public, ...]] */ protected $publicMethods = []; /** * @param \bff\contracts\Application $app * @param \Illuminate\Contracts\Hashing\Hasher $hasher */ public function __construct(ApplicationContract $app, HasherContract $hasher) { $this->app = $app; $this->hasher = $hasher; } /** * X-Frame-Options & click-jacking attacks * @param string|null $options * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options */ public function setIframeOptions(?string $options = null) { $options = $options ?? $this->app->filter('security.iframe.options', 'sameorigin'); Response::withHeader('X-Frame-Options', mb_strtoupper(strval($options))); } /** * Общий хеш-ключ admin сессий для проекта * Для возможности удаления всех активных сессий * @param bool $refresh сформировать новый * @return string */ public function adminSessionsHash($refresh = false) { $key = 'security.admin.sessions.hash'; $hash = config::get($key, ''); if ($refresh || empty($hash)) { $hash = Str::random(32); config::save($key, $hash); } return $hash; } /** * Проверка CSRF токена (передаваемого пользователем вместе с запросом) * @param string|null $input * @return bool корректный ли токен */ public function validateToken(?string $input = null): bool { $token = Session::token(); if (empty($token)) { return true; } if ($this->app->isTest()) { return true; } if (is_null($input)) { $input = $this->app->input()->postget($this->tokenInput, TYPE_STR); } if ($input !== $token) { $this->app->log('security::validateToken failed: session token mismatch', Logger::INFO, Logger::DEFAULT_FILE, [ 'token' => $input, 'session_token' => $token, ]); return false; } return true; } /** * Проверка на корректность заголовок HTTP_REFERER * - должен быть с текущего домена или поддомена (SITEHOST) * @param string|null $referer заголовок для проверки или null (текущий) * @return bool */ public function validateReferer(?string $referer = null): bool { $logFail = function ($message, $context = []) { $this->app->log('security::validateReferer failed: ' . $message, Logger::INFO, Logger::DEFAULT_FILE, $context); }; do { if ($this->app->isTest()) { # Test mode enabled return true; } if (is_null($referer)) { $referer = $this->app->request()->referer(); if ($referer === '') { # No referer return true; } } else { if (empty($referer)) { $logFail('empty referer', ['referer' => $referer, 'request_referer' => $this->app->request()->referer()]); break; } } # некорректный домен / поддомен $referer = strtolower(preg_replace("[^http://|https://|ftp://]", '', $referer)); # UTF-8 domain names fix if (mb_stripos(SITEHOST, 'xn--') === 0) { $referer = preg_replace('/(' . preg_quote(idn_to_utf8(SITEHOST, INTL_IDNA_VARIANT_UTS46)) . ')/iu', SITEHOST, $referer); } if (SITEHOST != $referer && strpos($referer . '/', SITEHOST . '/') !== 0) { if (stripos($referer, SITEHOST) === false) { $logFail('referer mismatch', ['referer' => $referer, 'sitehost' => SITEHOST]); break; } } return true; } while (false); return false; } /** * Is administrator with access to module (and scope) * @param string $module * @param string|array|null $scope * @return bool */ public function haveAccessTo(string $module, $scope = null) { return ( User::logined() && User::hasAccessTo($module, $scope) ); } /** * Is administrator with access to module method. * @param \Module $module * @param string $method * @param string|null $action * @return bool */ public function haveAccessToModuleMethod(Module $module, string $method, ?string $action = null): bool { # Gather scopes to test $scopes = $this->getModuleMethodScopes($module, $method, $action); # Test access to module scope foreach ($scopes as $access) { if ($this->haveAccessTo($access[0], $access[1])) { return true; } } return false; } /** * Get module access scopes * @param Module $module * @return \bff\admin\AccessScopes */ public function getModuleAccessScopes(Module $module) { $scopes = $module->accessScopes(); if (is_array($scopes)) { $scopes = (new AccessScopes($module))->fromArray($scopes); } return $this->app->filter('security.module.access.scopes', $scopes, $module); } /** * Convert module method to scope. * @param \Module $module * @param string $method * @param string|null $action * @return array */ public function getModuleMethodScopes(Module $module, string $method, ?string $action = null) { return $this->getModuleAccessScopes($module)->getMethodScopes($method, $action); } /** * Mark method as public (direct call is allowed) * @param mixed $controller * @param string|string[] $method * @param bool|\Closure $public * @param string|null $context * @return bool */ public function publicMethod($controller, $method, $public = true, ?string $context = null) { if (is_array($method)) { foreach ($method as $m) { $this->publicMethod($controller, $m, $public, $context); } return true; } if ($controller instanceof Module) { $controller = $controller->module_name; } if (! is_scalar($controller)) { return false; } if (empty($controller) || empty($method)) { return false; } $method = mb_strtolower($method); if (! isset($this->publicMethods[$controller][$method])) { $this->publicMethods[$controller][$method] = []; } $this->publicMethods[$controller][$method][$context ?? $this->app->context()] = $public; return true; } /** * Mark method as not public (direct call is forbidden) * @param mixed $controller * @param string|string[] $method * @param string|null $context * @return bool */ public function unpublicMethod($controller, $method, ?string $context = null) { return $this->publicMethod($controller, $method, false, $context); } /** * Is method public and is available to call directly * @param mixed $controller * @param string $method * @param string|null $context * @return bool */ public function isPublicMethod($controller, string $method, ?string $context = null) { if ($controller instanceof Module) { $controller = $controller->module_name; } if (! is_scalar($controller)) { return false; } if (empty($controller) || empty($method)) { return false; } $method = mb_strtolower($method); $context = $context ?? $this->app->context(); if ($context === bff::CONTEXT_FRONTEND) { if (in_array($method, ['ajax'], true)) { return true; } } $public = $this->publicMethods[$controller][$method][$context] ?? false; if ($public instanceof Closure) { $public = $public(); } return ! empty($public); } /** * Hash the given password * @param string $password * @param string $salt * @return string */ public function passwordHash($password, $salt = '') { return $this->hasher->make($password); } /** * Check the given plain password against a hash * @param string $password plain password * @param string $hashed * @param string $salt * @return bool */ public function passwordCheck($password, $hashed, $salt = '') { if ($this->passwordsV2()) { return hash_equals( $hashed, $this->passwordHash($password, $salt) ); } return $this->hasher->check($password, $hashed); } /** * Validate password * @param string $password * @param array $opts [ * 'min' => 4, * 'title' => 'Password', * 'input' => 'password', * 'noSpaces' => true, # no spaces allowed * 'noTags' => true, # no html tags allowed * 'noUrl' => true, # no urls allowed * ] * @return bool */ public function validatePassword($password, array $opts = []): bool { $opts = array_merge([ 'silent' => false, 'title' => _t('', 'Password'), 'input' => 'password', ], $opts); $rule = new Password($opts); $valid = $rule->check($password); if (! $valid && ! $opts['silent']) { Errors::set($rule->getMessage($opts['title']), $opts['input']); } return $valid; } /** * Generate new password * @param int $length * @return string */ public function generatePassword(int $length = 12): string { return func::generator($length); } /** * Generate password salt * @param int $length * @return string */ public function generatePasswordSalt(int $length = 4): string { return func::generator($length, false); } /** * Deprecated (V2) passwords hash generator enabled * @return bool */ protected function passwordsV2() { return config::get('security.passwords.v2', false); } /** * Шифрует данные по ключу * @param string|array $data данные которые необходимо зашифровать * @return string зашифрованные данные * @throws \Illuminate\Contracts\Encryption\EncryptException */ public function encrypt($data) { return Crypt::encrypt($data, ! is_scalar($data)); } /** * Расшифровывает данные по ключу * @param string $payload данные для расшифровки * @param bool $unserialize * @return mixed расшифрованные данные * @throws \Illuminate\Contracts\Encryption\DecryptException */ public function decrypt(string $payload, bool $unserialize = false): string { try { return Crypt::decrypt($payload, $unserialize); } catch (DecryptException $e) { return config::decrypt($payload); } } /** * Сравнение двух строк методом более устойчивым к "атаке по времени" * @param string $original ожидаемая строка * @param string $input строка для сравнения * @return bool true - строки одинаковые, false - разные */ public function compareString(string $original, string $input): bool { return hash_equals($original, $input); } }