From 9dd0473359aec5dd8c01e42abaecbda6b9f1e0b8 Mon Sep 17 00:00:00 2001 From: Noah Lancien Date: Tue, 24 Mar 2026 16:02:15 +0100 Subject: [PATCH] fix: handmade admin-secure while waiting for bump/remake --- .env.example | 8 +- app/Http/Kernel.php | 1 + app/Http/Middleware/AdminSecureMiddleware.php | 49 ++++++ app/Lib/AdminSecure.php | 131 +++++++++++++++ app/Lib/Options.php | 159 ++++++++++++++++++ composer.json | 2 +- composer.lock | 89 ++-------- config/admin-secure.php | 6 + manifest.yaml | 1 + 9 files changed, 364 insertions(+), 82 deletions(-) create mode 100644 app/Http/Middleware/AdminSecureMiddleware.php create mode 100644 app/Lib/AdminSecure.php create mode 100644 app/Lib/Options.php create mode 100644 config/admin-secure.php diff --git a/.env.example b/.env.example index 5ca6394..3e22b68 100644 --- a/.env.example +++ b/.env.example @@ -61,6 +61,8 @@ GOOGLE_RECAPTCHA_PUBLIC= GOOGLE_RECAPTCHA_PRIVATE= GOOGLE_RECAPTCHA_VERSION= -# ADMIN SECURE -# - API KEY -LDC_WEB_API_KEY="" +# UWIL ADMIN SECURE +# - LIST SEPARATED WITH ',' (Required) +UWIL_ADMIN_SECURE_CIDR_LIST="" +# - LIST SEPARATED WITH ',' (Optional) +UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST="" diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 09fd3fe..1d8b268 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -67,5 +67,6 @@ class Kernel extends HttpKernel 'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class, 'crm_questions' => \App\Http\Middleware\CrmMiddlewareQuestions::class, 'json_only' => \App\Http\Middleware\JsonOnly::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..1851b93 --- /dev/null +++ b/app/Http/Middleware/AdminSecureMiddleware.php @@ -0,0 +1,49 @@ +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..9a3de64 --- /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) use ($ipv6_address) { + 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 074aef3..3e32f79 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "php": "^8.2", "axlon/laravel-postal-code-validation": "^3.4", "diglactic/laravel-breadcrumbs": "^8.0", + "dxw/cidr": "^3.2", "guzzlehttp/guzzle": "^7.2", "jenssegers/agent": "^2.6", "laravel/framework": "^10.0", @@ -14,7 +15,6 @@ "laravel/ui": "^4.2", "ldc/ldccrmv4client": "^4.0", "ldc/sitemap-generator": "^2.0", - "ldc/web-admin-secure-laravel": "^2.0", "mews/purifier": "^3.4", "propaganistas/laravel-phone": "^5.0", "sentry/sentry-laravel": "*", diff --git a/composer.lock b/composer.lock index 74f502e..06fb3ee 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": "a35ca20936b03488df87a0399d659335", + "content-hash": "45214b0580fa4955ba3c2dc5a8c27c5e", "packages": [ { "name": "axlon/laravel-postal-code-validation", @@ -2325,73 +2325,6 @@ "Sitemap" ] }, - { - "name": "ldc/web-admin-secure-laravel", - "version": "2.0.3", - "source": { - "type": "git", - "url": "git@lab.s-web.fr:ldc-web/projets/ldc-web-admin-secure-laravel.git", - "reference": "fc648612f0e16ca00c012e5305b28058312d68c0" - }, - "dist": { - "type": "zip", - "url": "https://lab.s-web.fr/api/v4/projects/20/packages/composer/archives/ldc/web-admin-secure-laravel.zip?sha=fc648612f0e16ca00c012e5305b28058312d68c0", - "reference": "fc648612f0e16ca00c012e5305b28058312d68c0", - "shasum": "" - }, - "require": { - "dxw/cidr": "^3.1", - "illuminate/support": "~10", - "laravel/framework": "~10", - "php": ">=8.2", - "symfony/options-resolver": "^3.4.43|^4.4.30|^5.0.11|^6.0" - }, - "require-dev": { - "larastan/larastan": "^2.6", - "orchestra/testbench": "^8", - "phpunit/php-code-coverage": "^9", - "phpunit/phpunit": "^9", - "slevomat/coding-standard": "^7", - "squizlabs/php_codesniffer": "^3" - }, - "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": "2.6.1", @@ -3743,16 +3676,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.48", + "version": "2.0.52", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "eaa7be704b8b93a6913b69eb7f645a59d7731b61" + "reference": "2552c4001631d1cc844332faea6a08a49c964b28" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/eaa7be704b8b93a6913b69eb7f645a59d7731b61", - "reference": "eaa7be704b8b93a6913b69eb7f645a59d7731b61", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2552c4001631d1cc844332faea6a08a49c964b28", + "reference": "2552c4001631d1cc844332faea6a08a49c964b28", "shasum": "" }, "require": { @@ -3760,7 +3693,7 @@ }, "require-dev": { "phing/phing": "~2.7", - "phpunit/phpunit": "^4.8.35|^5.7|^6.0|^9.4", + "phpunit/phpunit": "^4.8.35|^5.7|^6.0|^8.5|^9.4", "squizlabs/php_codesniffer": "~2.0" }, "suggest": { @@ -3833,7 +3766,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/2.0.48" + "source": "https://github.com/phpseclib/phpseclib/tree/2.0.52" }, "funding": [ { @@ -3849,7 +3782,7 @@ "type": "tidelift" } ], - "time": "2024-12-14T21:03:54+00:00" + "time": "2026-03-19T02:54:44+00:00" }, { "name": "propaganistas/laravel-phone", @@ -11979,12 +11912,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.2" }, - "platform-dev": {}, - "plugin-api-version": "2.6.0" + "platform-dev": [], + "plugin-api-version": "2.3.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 c8f6fd1..d4d34a5 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -39,6 +39,7 @@ data: MAIL_CONTACT_PARTNERSHIP: "${APP_MAIL_CONTACT_PARTNERSHIP}" MAIL_CONTACT_COMMERCIAL: "${APP_MAIL_CONTACT_COMMERCIAL}" MAIL_CONTACT_VARIOUS: "${APP_MAIL_CONTACT_VARIOUS}" + UWIL_ADMIN_SECURE_CIDR_LIST: "${APP_GLOBAL_UWIL_ADMIN_SECURE_CIDR_LIST}" --- apiVersion: v1