Merge branch 'develop'
This commit is contained in:
commit
5f0c6b136f
12 changed files with 330 additions and 52 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,4 +3,5 @@ vendor
|
|||
.phpunit.result.cache
|
||||
.phpunit.cache
|
||||
.vscode/launch.json
|
||||
composer.lock
|
||||
build
|
||||
|
|
|
|||
|
|
@ -7,7 +7,13 @@ composer req ldc/web-admin-secure-laravel:1.0.1
|
|||
.env.example :
|
||||
|
||||
```dotenv
|
||||
LDC_WEB_API_KEY="MY_KEY"
|
||||
# ADMIN SECURE
|
||||
# - API ENDPOINT (Optional)
|
||||
LDC_WEB_API_ENDPOINT="https://ldc-web.ldc.fr/api/ldc-networks"
|
||||
# - API KEY
|
||||
LDC_WEB_API_KEY="CE8S52Qq7stf5k8frF4T"
|
||||
# - LIST SEPARATED WITH ',' (Optional)
|
||||
LDC_WEB_CIDR_MANUAL_ALLOWED_LIST=""
|
||||
```
|
||||
|
||||
README.md :
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## **Version 2.0.0**
|
||||
*Changed*
|
||||
- Compatible only with **php 8.2**
|
||||
- Allow connections from private ip ranges
|
||||
- Allow setting Cidr list into config
|
||||
## **Version 1.0.1**
|
||||
*Changed*
|
||||
- Run scheduled only on production envirronement
|
||||
|
|
|
|||
|
|
@ -10,16 +10,17 @@
|
|||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "~7.3|7.4|~8",
|
||||
"illuminate/support": "~8|~9",
|
||||
"laravel/framework": "~8|~9"
|
||||
"php": ">=8.2",
|
||||
"illuminate/support": "~10",
|
||||
"laravel/framework": "~10",
|
||||
"dxw/cidr": "^3.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~9.0",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"orchestra/testbench": "^6.24",
|
||||
"slevomat/coding-standard": "^7.0",
|
||||
"phpunit/php-code-coverage": "^9.2"
|
||||
"phpunit/phpunit": "^9",
|
||||
"squizlabs/php_codesniffer": "^3",
|
||||
"orchestra/testbench": "^8",
|
||||
"slevomat/coding-standard": "^7",
|
||||
"phpunit/php-code-coverage": "^9"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'endpoint' => env('LDC_WEB_API_ENDPOINT', 'https://ldc-web.ldc.fr/api/ldc-networks'),
|
||||
'apiKey' => env('LDC_WEB_API_KEY'),
|
||||
'cidrManualAllowedList' => collect(explode(',', strval(env(
|
||||
'LDC_WEB_CIDR_MANUAL_ALLOWED_LIST'
|
||||
))))->filter()
|
||||
];
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
<rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
|
||||
<rule ref="Generic.CodeAnalysis.UnusedFunctionParameter"/>
|
||||
<rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
|
||||
<rule ref="Generic.Commenting.DocComment.SpacingBeforeTags"/>
|
||||
<rule ref="Generic.Commenting.Todo"/>
|
||||
<rule ref="Generic.Commenting.Fixme"/>
|
||||
<rule ref="Generic.ControlStructures.InlineControlStructure"/>
|
||||
|
|
@ -114,4 +115,10 @@
|
|||
<rule ref="Squiz.Commenting.PostStatementComment"/>
|
||||
<rule ref="Squiz.Commenting.VariableComment"/>
|
||||
<rule ref="SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse" />
|
||||
|
||||
<!-- For Laravel migrations -->
|
||||
<rule ref="PSR1.Classes.ClassDeclaration.MissingNamespace">
|
||||
<exclude-pattern>*/database/migrations/*</exclude-pattern>
|
||||
</rule>
|
||||
|
||||
</ruleset>
|
||||
|
|
|
|||
249
src/AdminSecure.php
Normal file
249
src/AdminSecure.php
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
<?php
|
||||
|
||||
namespace LDC\WebAdminSecureLaravel;
|
||||
|
||||
use Dxw\CIDR\IPAddress;
|
||||
use Dxw\CIDR\IPv4Address;
|
||||
use Dxw\CIDR\IPv4Range;
|
||||
use Dxw\CIDR\IPv6Address;
|
||||
use Dxw\CIDR\IPv6Range;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use LDC\WebAdminSecureLaravel\Exceptions\AdminSecureException;
|
||||
use LDC\WebAdminSecureLaravel\Exceptions\AdminSecureInvalidAdressException;
|
||||
|
||||
abstract class AdminSecure
|
||||
{
|
||||
/**
|
||||
* The cache key where to store ip list.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static string $cache_key = 'ldcweb_ip_list';
|
||||
|
||||
/**
|
||||
* The cache key where to store config cidr list.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static string $cache_config_key = 'ldcweb_ip_cidr_config_list';
|
||||
|
||||
/**
|
||||
* Private ipV4 CIDR list
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static array $private_ip_v4_cidr_list = [
|
||||
'10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'
|
||||
];
|
||||
|
||||
/**
|
||||
* Private ipV4 CIDR loopback
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static string $private_ip_v4_loopback_cidr = '127.0.0.0/8';
|
||||
|
||||
/**
|
||||
* Private ipV6 CIDR list
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static array $private_ip_v6_cidr_list = [
|
||||
'fd00::/8'
|
||||
];
|
||||
|
||||
/**
|
||||
* Private ipV6 CIDR loopback
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static string $private_ip_v6_loopback_cidr = '::1/128';
|
||||
|
||||
/**
|
||||
* Check if ip is private
|
||||
*
|
||||
* @param \Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address $ip_address
|
||||
* @return boolean
|
||||
*/
|
||||
public static function ipIsPrivate(IPv4Address|IPv6Address $ip_address): bool
|
||||
{
|
||||
return \get_class($ip_address) === IPv4Address::class ?
|
||||
self::ipV4IsPrivate($ip_address) : self::ipV6IsPrivate($ip_address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch client IP
|
||||
*
|
||||
* @return \Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address
|
||||
* @throws \LDC\WebAdminSecureLaravel\AdminSecureException If the ip is invalid.
|
||||
*/
|
||||
public static function fetchClientIp(): IPv4Address|IPv6Address
|
||||
{
|
||||
// ! HA PROXY HACK.
|
||||
$ip = request()->server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp();
|
||||
$ip = IPAddress::Make($ip);
|
||||
|
||||
if ($ip->isErr()) {
|
||||
throw new AdminSecureException(\trans('Client IP not valid ') . $ip->getErr());
|
||||
}
|
||||
return $ip->unwrap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Ip Adress list from LDC Web
|
||||
*
|
||||
* @return array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address>
|
||||
* @throws \LDC\WebAdminSecureLaravel\AdminSecureException If Wrong env value.
|
||||
* @throws \Illuminate\Http\Client\RequestException If calling admin secure fails.
|
||||
* @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
*/
|
||||
public static function fetchIpList(): array
|
||||
{
|
||||
// phpcs:enable
|
||||
$endpoint = config('admin-secure.endpoint');
|
||||
$key = config('admin-secure.apiKey');
|
||||
if (!$endpoint or !\filter_var($endpoint, \FILTER_VALIDATE_URL)) {
|
||||
throw new AdminSecureException('LDC_WEB_API_ENDPOINT is required in .env to ba a valid url.');
|
||||
}
|
||||
if (!$key) {
|
||||
throw new AdminSecureException('LDC_WEB_API_KEY is required in .env .');
|
||||
}
|
||||
|
||||
$response = Http::get($endpoint, ['token' => $key])->throw();
|
||||
$adresses = self::parseAdresses($response);
|
||||
|
||||
Log::info(\sprintf('ADMINSECURE : Fetched %d ip(s) from LDC WEB', \count($adresses)));
|
||||
|
||||
return $adresses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Ip Adress list from LDC Web
|
||||
*
|
||||
* @return array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range>
|
||||
* @throws \LDC\WebAdminSecureLaravel\AdminSecureException If Wrong env value.
|
||||
* @phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
*/
|
||||
public static function fetchConfigAllowedCidrList(): array
|
||||
{
|
||||
$allowed_cidr_config_list = \collect(config('admin-secure.cidrManualAllowedList'))->filter();
|
||||
$allowed_cidr_config_list_hash = \md5($allowed_cidr_config_list->implode(','));
|
||||
$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 AdminSecureException(\trans(
|
||||
'LDC_WEB_CIDR_MANUAL_ALLOWED_LIST not valid (ipv4 or ipv6 cidr) :cidr',
|
||||
[
|
||||
'cidr' => $cidr
|
||||
]
|
||||
));
|
||||
}
|
||||
return $ip_range->unwrap();
|
||||
})->all();
|
||||
|
||||
// * Flush cache if hash config hash changed
|
||||
if ($allowed_cidr_config_list_hash !== \collect(Cache::get(self::$cache_config_key))->get('hash')) {
|
||||
Cache::flush(self::$cache_config_key);
|
||||
}
|
||||
|
||||
/** @var array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range> $allowed_cidr_list */
|
||||
return \collect(Cache::rememberForever(self::$cache_config_key, fn () => [
|
||||
'list' => $allowed_cidr_list,
|
||||
'hash' => $allowed_cidr_config_list_hash
|
||||
]))->get('list');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ip list from cache
|
||||
*
|
||||
* @return array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address>
|
||||
*/
|
||||
public static function getCacheList(): array
|
||||
{
|
||||
try {
|
||||
$ip_list = Cache::get(self::$cache_key, self::fetchIpList());
|
||||
self::validateIpList($ip_list);
|
||||
/** @var array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address> $ip_list */
|
||||
return $ip_list;
|
||||
} catch (AdminSecureInvalidAdressException) {
|
||||
$ip_list = self::fetchIpList();
|
||||
self::validateIpList($ip_list);
|
||||
self::setCacheList($ip_list);
|
||||
return $ip_list;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ip list to cache
|
||||
*
|
||||
* @param array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address> $ip_list
|
||||
* @return void
|
||||
*/
|
||||
public static function setCacheList(array $ip_list): void
|
||||
{
|
||||
self::validateIpList($ip_list);
|
||||
Cache::put(self::$cache_key, $ip_list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(fn (string $ipv4_cidr) => IPv4Range::Make($ipv4_cidr)->unwrap())
|
||||
->reduce(fn (bool|null $initial, IPv4Range $ipv4_range) =>
|
||||
$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(fn (string $ipv6_cidr) => IPv6Range::Make($ipv6_cidr)->unwrap())
|
||||
->reduce(fn (bool|null $initial, IPv6Range $ipv6_range) =>
|
||||
$initial || $ipv6_range->containsAddress($ipv6_address));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate ip list
|
||||
*
|
||||
* @param array $ip_list
|
||||
* @return void
|
||||
* @throws \LDC\WebAdminSecureLaravel\Exceptions\AdminSecureInvalidAdressException If an IP is invalid.
|
||||
*/
|
||||
private static function validateIpList(array $ip_list): void
|
||||
{
|
||||
\collect($ip_list)->each(function ($ip) {
|
||||
if (!\is_object($ip) or !\in_array(\get_class($ip), [IPv4Address::class, IPv6Address::class])) {
|
||||
throw new AdminSecureInvalidAdressException('Invalid adresse ' . \strval(\json_encode($ip)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse adresses
|
||||
*
|
||||
* @param \Illuminate\Http\Client\Response $response
|
||||
* @return array<\Dxw\CIDR\IPAddress>
|
||||
*/
|
||||
private static function parseAdresses(Response $response): array
|
||||
{
|
||||
return \collect($response->json())
|
||||
->map(fn (string $ip) => IPAddress::Make($ip)->unwrap())->all();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
namespace LDC\WebAdminSecureLaravel;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Routing\Router;
|
||||
use LDC\WebAdminSecureLaravel\Console\SyncFromLDCWeb;
|
||||
|
|
@ -21,22 +22,22 @@ class AdminSecureServiceProvider extends ServiceProvider
|
|||
__DIR__ . '/../config' => config_path(),
|
||||
], 'config');
|
||||
|
||||
// Register the command if we are using the application via the CLI .
|
||||
if ($this->app->runningInConsole()) {
|
||||
$this->commands([
|
||||
SyncFromLDCWeb::class,
|
||||
]);
|
||||
}
|
||||
// * Register the command if we are using the application via the CLI .
|
||||
$this->commands([SyncFromLDCWeb::class]);
|
||||
|
||||
// * Set filter condition
|
||||
$this->app->singleton('hasToFilterIp', fn (Application $app) => $app->environment('production'));
|
||||
|
||||
// * Cron Report everyday
|
||||
$this->app->booted(function () {
|
||||
if ($this->app->environment('production')) {
|
||||
if ($this->app->make('hasToFilterIp')) {
|
||||
/** @var \Illuminate\Console\Scheduling\Schedule */
|
||||
$schedule = $this->app->make(Schedule::class);
|
||||
$schedule->command('ldcweb:admin-secure-sync')->daily();
|
||||
}
|
||||
});
|
||||
|
||||
/** @var \Illuminate\Routing\Router */
|
||||
$router = $this->app->make(Router::class);
|
||||
$router->aliasMiddleware('admin-secure', AdminSecureMiddleware::class);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@
|
|||
namespace LDC\WebAdminSecureLaravel\Console;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use LDC\WebAdminSecureLaravel\AdminSecureException;
|
||||
use LDC\WebAdminSecureLaravel\AdminSecure;
|
||||
|
||||
/**
|
||||
* Send Report Status To LDC Web.
|
||||
*
|
||||
* @author Vincent BRIDIER <vincent.bridier@ldc.fr>
|
||||
*/
|
||||
class SyncFromLDCWeb extends Command
|
||||
|
|
@ -25,36 +24,18 @@ class SyncFromLDCWeb extends Command
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Sync IP ranges from LDC Web';
|
||||
|
||||
/**
|
||||
* The call endpoint
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $endpoint = 'https://ldc-web.ldc.fr/api/ldc-networks';
|
||||
protected $description = 'Sync IP list from LDC Web';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return integer
|
||||
* @throws \LDC\WebAdminSecureLaravel\Exceptions\AdminSecureException If missing LDC_WEB_API_KEY key.
|
||||
*/
|
||||
public function handle()
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Sync IP Ranges from LDC Web.');
|
||||
|
||||
if (!config('admin-secure.apiKey')) {
|
||||
throw new AdminSecureException('LDC_WEB_API_KEY is required in .env');
|
||||
}
|
||||
|
||||
$response = Http::get($this->endpoint, [
|
||||
'token' => config('admin-secure.apiKey')
|
||||
]);
|
||||
|
||||
$response->throw();
|
||||
|
||||
Cache::put('ldcweb_ip_ranges', \json_decode($response->body()));
|
||||
|
||||
$this->info($this->description);
|
||||
AdminSecure::setCacheList(AdminSecure::fetchIpList());
|
||||
$this->info('Done.');
|
||||
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace LDC\WebAdminSecureLaravel;
|
||||
namespace LDC\WebAdminSecureLaravel\Exceptions;
|
||||
|
||||
class AdminSecureException extends \Exception
|
||||
{
|
||||
7
src/Exceptions/AdminSecureInvalidAdressException.php
Normal file
7
src/Exceptions/AdminSecureInvalidAdressException.php
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace LDC\WebAdminSecureLaravel\Exceptions;
|
||||
|
||||
class AdminSecureInvalidAdressException extends AdminSecureException
|
||||
{
|
||||
}
|
||||
|
|
@ -3,31 +3,46 @@
|
|||
namespace LDC\WebAdminSecureLaravel\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Dxw\CIDR\IP;
|
||||
use Dxw\CIDR\IPv4Address;
|
||||
use Dxw\CIDR\IPv4Range;
|
||||
use Dxw\CIDR\IPv6Address;
|
||||
use Dxw\CIDR\IPv6Range;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use LDC\WebAdminSecureLaravel\AdminSecure;
|
||||
|
||||
class AdminSecureMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (\config('app.env') === 'local') {
|
||||
if (!\app()->make('hasToFilterIp')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/** HA PROXY HACK */
|
||||
$ip = request()->server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp();
|
||||
$restrictedIps = Cache::get('ldcweb_ip_ranges');
|
||||
$client_ip = AdminSecure::fetchClientIp();
|
||||
$ip_is_allowed = \collect(AdminSecure::fetchIpList())
|
||||
->reduce(fn (bool|null $initial, IPv4Address|IPv6Address $ip_address) => $initial || IP::contains(
|
||||
$ip_address,
|
||||
$client_ip
|
||||
)->unwrap());
|
||||
|
||||
if (!is_null($restrictedIps) && in_array($ip, $restrictedIps)) {
|
||||
$ip_is_allowed_by_config = \collect(AdminSecure::fetchConfigAllowedCidrList())
|
||||
->reduce(fn (bool|null $initial, IPv4Range|IPv6Range $ip_range) => $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);
|
||||
|
||||
abort(403, \trans('Denied IP'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue