diff --git a/.env.example b/.env.example index ca093e9..a6f0193 100644 --- a/.env.example +++ b/.env.example @@ -40,9 +40,12 @@ MATOMO_SITE_ID= GOOGLE_RECAPTCHA_PUBLIC= GOOGLE_RECAPTCHA_PRIVATE= GOOGLE_RECAPTCHA_VERSION= +# UWIL ADMIN SECURE +# - LIST SEPARATED WITH ',' (Required) +UWIL_ADMIN_SECURE_CIDR_LIST="" +# - LIST SEPARATED WITH ',' (Optional) +UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST="" -## LDCWEB ADMIN SECURE -LDC_WEB_API_KEY= # Sentry SENTRY_LARAVEL_DSN= diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index baed7a9..98e3607 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -68,5 +68,6 @@ class Kernel extends HttpKernel 'lang-check' => LangCheck::class, 'front-end-globals' => FrontEndGlobals::class, 'back-end-globals' => BackEndGlobals::class, + 'admin-secure' => \App\Http\Middleware\AdminSecureMiddleware::class, ]; } diff --git a/app/Http/Middleware/AdminSecureMiddleware.php b/app/Http/Middleware/AdminSecureMiddleware.php new file mode 100644 index 0000000..a2ac87f --- /dev/null +++ b/app/Http/Middleware/AdminSecureMiddleware.php @@ -0,0 +1,50 @@ +getCidrList()) + ->reduce(function ($initial, $ip_range) use ($client_ip) { + return $initial || IP::contains( + $ip_range, + $client_ip + )->unwrap(); + }); + + $ip_is_allowed_by_config = \collect(AdminSecure::getOptions()->getCidrAllowedList()) + ->reduce(function ($initial, $ip_range) use ($client_ip) { + return $initial || IP::contains( + $ip_range, + $client_ip + )->unwrap(); + }); + + if ($ip_is_allowed or $ip_is_allowed_by_config or AdminSecure::ipIsPrivate($client_ip)) { + return $next($request); + } + + abort(403, \trans('Denied IP')); + } +} + diff --git a/app/Lib/AdminSecure.php b/app/Lib/AdminSecure.php new file mode 100644 index 0000000..cc05a57 --- /dev/null +++ b/app/Lib/AdminSecure.php @@ -0,0 +1,131 @@ +server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp(); + $ip = IPAddress::Make($ip); + + if ($ip->isErr()) { + throw new \Exception(\trans('Client IP not valid ') . $ip->getErr()); + } + return $ip->unwrap(); + } + + /** + * Check if ipV4 is private + * + * @param \Dxw\CIDR\IPv4Address $ipv4_address + * @return boolean + */ + private static function ipV4IsPrivate(IPv4Address $ipv4_address): bool + { + return \collect(self::$private_ip_v4_cidr_list) + ->concat([self::$private_ip_v4_loopback_cidr]) + ->map(function (string $ipv4_cidr) { + return IPv4Range::Make($ipv4_cidr)->unwrap(); + }) + ->reduce(function ($initial, IPv4Range $ipv4_range) use ($ipv4_address) { + return $initial || $ipv4_range->containsAddress($ipv4_address); + }); + } + + /** + * Check if ipV6 is private + * + * @param \Dxw\CIDR\IPv6Address $ipv6_address + * @return boolean + */ + private static function ipV6IsPrivate(IPv6Address $ipv6_address): bool + { + return \collect(self::$private_ip_v6_cidr_list) + ->concat([self::$private_ip_v6_loopback_cidr]) + ->map(function (string $ipv6_cidr) { + return IPv6Range::Make($ipv6_cidr)->unwrap(); + }) + ->reduce(function ($initial, IPv6Range $ipv6_range) { + return $initial || $ipv6_range->containsAddress($ipv6_address); + }); + } + + /** + * Get Admin secure options + * + * @return \App\Lib\Options + */ + public static function getOptions(): Options + { + if (!self::$options) { + self::$options = new Options(\config('admin-secure')); + } + return self::$options; + } +} diff --git a/app/Lib/Options.php b/app/Lib/Options.php new file mode 100644 index 0000000..b627087 --- /dev/null +++ b/app/Lib/Options.php @@ -0,0 +1,159 @@ + The configuration options + */ + private $options; + + /** + * @var \Symfony\Component\OptionsResolver\OptionsResolver The options resolver + */ + private $resolver; + + /** + * The cache key where to store config cidr list. + * + * @var string + */ + private static $cache_config_key = 'uwil_ip_cidr_config_list'; + + /** + * Class constructor. + * + * @param array $options The configuration options. + */ + public function __construct(array $options = []) + { + $this->resolver = new OptionsResolver(); + + $this->configureOptions($this->resolver); + + $this->options = $this->resolver->resolve($options); + } + + /** + * Get cidr list + * + * @return array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range> + */ + public function getCidrList(): array + { + return $this->options['cidrList']; + } + + /** + * Get cidr project allowed list + * + * @return array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range> + */ + public function getCidrAllowedList(): array + { + return $this->options['cidrProjectAllowedList']; + } + + /** + * Configures the options of the client. + * + * @param \Symfony\Component\OptionsResolver\OptionsResolver $resolver The resolver for the options. + * @return void + */ + private function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'cidrList' => [], + 'cidrProjectAllowedList' => [] + ]); + + $resolver->setNormalizer('cidrList', \Closure::fromCallable([$this, 'normalizeCidrList'])); + $resolver->setNormalizer('cidrProjectAllowedList', \Closure::fromCallable([$this, 'normalizeCidrProjectList'])); + } + + /** + * Normalise Cidr list for cidrList option + * + * @param \Symfony\Component\OptionsResolver\Options $options + * @param string|null $value + * @return array + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed + */ + private function normalizeCidrList( + \Symfony\Component\OptionsResolver\Options $options, + ?string $value + ): array { + // phpcs:enable + return $this->doNormalizeCidrList($value, 'cidrList'); + } + + /** + * Normalise Cidr list for cidrProjectAllowedList option + * + * @param \Symfony\Component\OptionsResolver\Options $options + * @param string|null $value + * @return array + * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed + */ + private function normalizeCidrProjectList( + \Symfony\Component\OptionsResolver\Options $options, + ?string $value + ): array { + // phpcs:enable + return $this->doNormalizeCidrList($value, 'cidrProjectAllowedList'); + } + + /** + * Normalize CIDR list with specific cache key. + * + * @param string|null $value + * @param string $optionName + * @return array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range> + */ + private function doNormalizeCidrList(?string $value, string $optionName): array + { + $cacheConfigKey = self::$cache_config_key . '_' . $optionName; + $allowed_cidr_config_list = collect(explode(',', strval($value)))->filter(); + $allowed_cidr_config_list_hash = \md5($allowed_cidr_config_list->implode(',')); + $allowed_cidr_list = []; + + // Flush cache if config hash changed + if ($allowed_cidr_config_list_hash !== \collect(Cache::get($cacheConfigKey))->get('hash')) { + Cache::forget($cacheConfigKey); + + /** @var array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range> $allowed_cidr_list */ + $allowed_cidr_list = $allowed_cidr_config_list + ->map(function (string $cidr) { + $ip_range = !IPv4Range::Make($cidr)->isErr() ? IPv4Range::Make($cidr) : IPv6Range::Make($cidr); + if ($ip_range->isErr()) { + throw new \Exception(\trans( + 'CIDR LIST not valid (ipv4 or ipv6 cidr) :cidr', + [ + 'cidr' => $cidr + ] + )); + } + return $ip_range->unwrap(); + })->all(); + } + + /** @var array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range> $allowed_cidr_list */ + return \collect(Cache::rememberForever($cacheConfigKey, function() use ($allowed_cidr_list, $allowed_cidr_config_list_hash) { + return [ + 'list' => $allowed_cidr_list, + 'hash' => $allowed_cidr_config_list_hash + ]; + }))->get('list'); + } +} diff --git a/composer.json b/composer.json index 336a425..098f573 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "license": "MIT", "require": { "php": "^7.3", + "dxw/cidr": "^3.1", "fruitcake/laravel-cors": "^2.0", "guzzlehttp/guzzle": "^7.0.1", "joedixon/laravel-translation": "^1.1", @@ -17,7 +18,6 @@ "laravel/framework": "^8.65", "laravel/tinker": "^2.5", "laravel/ui": "^3.3", - "ldc/web-admin-secure-laravel": "^1.0", "mews/purifier": "^3.3", "sentry/sentry-laravel": "^4.1", "spatie/laravel-translatable": "^4.6", diff --git a/composer.lock b/composer.lock index 568b029..a8587eb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d6927322e99a0f1d530d53690641962b", + "content-hash": "bf80b4c19ea353a96fe885a27183dea9", "packages": [ { "name": "asm89/stack-cors", @@ -423,6 +423,107 @@ ], "time": "2024-10-09T13:47:03+00:00" }, + { + "name": "dxw/cidr", + "version": "v3.1.2", + "source": { + "type": "git", + "url": "https://github.com/dxw/cidr.git", + "reference": "9cb82cb70f0d4a4a32b1b2e3e1ae005fba84d581" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dxw/cidr/zipball/9cb82cb70f0d4a4a32b1b2e3e1ae005fba84d581", + "reference": "9cb82cb70f0d4a4a32b1b2e3e1ae005fba84d581", + "shasum": "" + }, + "require": { + "dxw/result": "^1.0", + "php": "^7.2", + "phpseclib/phpseclib": "^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "mockery/mockery": "^1.3", + "peridot-php/leo": "^1.6", + "peridot-php/peridot": "^1.19", + "peridot-php/peridot-dot-reporter": "^1.0", + "vimeo/psalm": "^4.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dxw\\CIDR\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "dxw", + "email": "team@govpress.com", + "homepage": "https://www.dxw.com" + } + ], + "description": "IP address matching with CIDR ranges", + "homepage": "https://github.com/dxw/cidr", + "support": { + "issues": "https://github.com/dxw/cidr/issues", + "source": "https://github.com/dxw/cidr/tree/v3.1.2" + }, + "time": "2021-04-13T15:13:30+00:00" + }, + { + "name": "dxw/result", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/dxw/result.git", + "reference": "5b9d3988c215dc95e17cd082a33bc2c02b6007ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dxw/result/zipball/5b9d3988c215dc95e17cd082a33bc2c02b6007ca", + "reference": "5b9d3988c215dc95e17cd082a33bc2c02b6007ca", + "shasum": "" + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "peridot-php/leo": "^1.6", + "peridot-php/peridot": "^1.19", + "peridot-php/peridot-dot-reporter": "^1.0", + "vimeo/psalm": "^3.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dxw\\Result\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mallory Adams", + "email": "mallory@dxw.com", + "homepage": "https://www.dxw.com/" + } + ], + "description": "Result values", + "homepage": "https://github.com/dxw/result", + "support": { + "issues": "https://github.com/dxw/result/issues", + "source": "https://github.com/dxw/result/tree/master" + }, + "time": "2020-06-12T16:01:47+00:00" + }, { "name": "egulias/email-validator", "version": "2.1.25", @@ -1682,70 +1783,6 @@ }, "time": "2022-05-20T13:38:08+00:00" }, - { - "name": "ldc/web-admin-secure-laravel", - "version": "1.0.1", - "source": { - "type": "git", - "url": "git@lab.s-web.fr:ldc-web/projets/ldc-web-admin-secure-laravel.git", - "reference": "c538f5d349b119ac64cb62b33e6368bef6be90bc" - }, - "dist": { - "type": "zip", - "url": "https://lab.s-web.fr/api/v4/projects/20/packages/composer/archives/ldc/web-admin-secure-laravel.zip?sha=c538f5d349b119ac64cb62b33e6368bef6be90bc", - "reference": "c538f5d349b119ac64cb62b33e6368bef6be90bc", - "shasum": "" - }, - "require": { - "illuminate/support": "~8|~9", - "laravel/framework": "~8|~9", - "php": "~7.3|7.4|~8" - }, - "require-dev": { - "orchestra/testbench": "^6.24", - "phpunit/php-code-coverage": "^9.2", - "phpunit/phpunit": "~9.0", - "slevomat/coding-standard": "^7.0", - "squizlabs/php_codesniffer": "^3.5" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "LDC\\WebAdminSecureLaravel\\AdminSecureServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "LDC\\WebAdminSecureLaravel\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "LDC\\WebAdminSecureLaravel\\Tests\\": "tests" - } - }, - "scripts": { - "test": [ - "vendor/bin/phpunit" - ], - "post-autoload-dump": [ - "@php ./vendor/bin/testbench package:discover --ansi" - ] - }, - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Vincent BRIDIER", - "email": "vincent.bridier@ldc.fr", - "homepage": "https://www.ldc.fr" - } - ], - "description": "Restrict access to LDC Network" - }, { "name": "league/commonmark", "version": "1.6.7", @@ -2666,6 +2703,116 @@ ], "time": "2024-07-20T21:41:07+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "2.0.51", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "ed661e7cdaeb8c419e609e2f3203551a13c2ed48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/ed661e7cdaeb8c419e609e2f3203551a13c2ed48", + "reference": "ed661e7cdaeb8c419e609e2f3203551a13c2ed48", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phing/phing": "~2.7", + "phpunit/phpunit": "^4.8.35|^5.7|^6.0|^9.4", + "squizlabs/php_codesniffer": "~2.0" + }, + "suggest": { + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations.", + "ext-xml": "Install the XML extension to load XML formatted public keys." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/2.0.51" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2026-01-27T09:11:52+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -9852,12 +9999,12 @@ ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^7.3" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/config/admin-secure.php b/config/admin-secure.php new file mode 100644 index 0000000..e94639a --- /dev/null +++ b/config/admin-secure.php @@ -0,0 +1,6 @@ + env('UWIL_ADMIN_SECURE_CIDR_LIST'), + 'cidrProjectAllowedList' => env('UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST') +]; diff --git a/manifest.yaml b/manifest.yaml index 5bff760..abcc59c 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -22,7 +22,7 @@ data: MAIL_FROM_ADDRESS: "${APP_MAIL_FROM_ADDRESS}" MAIL_FROM_NAME: "${APP_APP_NAME}" MAIL_LIST_CONTACTFORM: "${APP_MAIL_LIST_CONTACTFORM}" - LDC_WEB_API_KEY: "${APP_LDC_WEB_API_KEY}" + UWIL_ADMIN_SECURE_CIDR_LIST: "${APP_GLOBAL_UWIL_ADMIN_SECURE_CIDR_LIST}" GOOGLE_RECAPTCHA_PRIVATE: "${APP_GOOGLE_RECAPTCHA_PRIVATE}" GOOGLE_RECAPTCHA_PUBLIC: "${APP_GOOGLE_RECAPTCHA_PUBLIC}" GOOGLE_RECAPTCHA_VERSION: "${APP_GOOGLE_RECAPTCHA_VERSION}"