feat: ✨ Remove LDC Web API dependency

This commit is contained in:
Vincent BRIDIER 2026-01-09 15:24:46 +01:00
parent 26f818b6b9
commit a4e0fafa16
11 changed files with 82 additions and 260 deletions

View file

@ -6,6 +6,12 @@ All notable changes to `admin-secure-laravel` will be documented in this file.
- This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
- Commits respect [Conventionnal commits](https://www.conventionalcommits.org/en/v1.0.0/) & use [Gitmoji](https://gitmoji.dev/).
## **[v5.0.0] - 09/01/2026**
### Added
- feat: ✨ Remove LDC Web API dependency
## **[v4.0.0] - 07/04/2025**
### Added

View file

@ -7,36 +7,19 @@ composer req uwil/admin-secure-laravel
.env.example :
```dotenv
# ADMIN SECURE
# - API ENDPOINT (Optional)
LDC_WEB_API_ENDPOINT="https://ldc-web.ldc.fr/api/ldc-networks"
# - API KEY
LDC_WEB_API_KEY=""
# UWIL ADMIN SECURE
# - LIST SEPARATED WITH ',' (Required)
UWIL_ADMIN_SECURE_CIDR_LIST=""
# - LIST SEPARATED WITH ',' (Optional)
LDC_WEB_CIDR_MANUAL_ALLOWED_LIST=""
UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST=""
```
README.md :
```
Put every min in PROD :
php artisan schedule:run
```
## Available Commands
```
uwil:admin-secure:sync
```
## Usage
Put 'admin-secure' middleware on route to protect it.
## Cron configure
Nothing to do command is already configureed to run everyday.
You just need to install this package.
**!! Runs only in production !!**
# Changelog
Latest version v4.0.0.
Latest version v5.0.0.
See the [CHANGELOG.md](CHANGELOG.md) file for details.

View file

@ -2,10 +2,10 @@
"name": "uwil/admin-secure-laravel",
"description": "Restrict access to LDC Network",
"license": "MIT",
"version": "4.0.0",
"version": "5.0.0",
"readme": "https://lab.s-web.fr/ldc-web/projets/ldc-web-admin-secure-laravel/-/blob/main/README.md",
"support": {
"email": "equipe.web@ldc.fr",
"email": "digital@ldc.fr",
"issues": "https://lab.s-web.fr/ldc-web/projets/ldc-web-admin-secure-laravel/-/issues"
},
"require": {

View file

@ -1,7 +1,6 @@
<?php
return [
'endpoint' => env('LDC_WEB_API_ENDPOINT', 'https://ldc-web.ldc.fr/api/ldc-networks'),
'apiKey' => env('LDC_WEB_API_KEY'),
'cidrManualAllowedList' => env('LDC_WEB_CIDR_MANUAL_ALLOWED_LIST')
'cidrList' => env('UWIL_ADMIN_SECURE_CIDR_LIST'),
'cidrProjectAllowedList' => env('UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST')
];

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 LDC Service Web
Copyright (c) 2022 LDC Digital
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -7,29 +7,10 @@ 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 Uwil\AdminSecure\Laravel\Exceptions\AdminSecureException;
use Uwil\AdminSecure\Laravel\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 time to live.
*
* @var string
*/
private static string $cache_ttl = '180 minutes';
/**
* Private ipV4 CIDR list
*
@ -89,7 +70,7 @@ abstract class AdminSecure
*/
public static function fetchClientIp(): IPv4Address|IPv6Address
{
// ! HA PROXY HACK.
// ! REVERSE PROXY HACK.
$ip = request()->server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp();
$ip = IPAddress::Make($ip);
@ -99,64 +80,6 @@ abstract class AdminSecure
return $ip->unwrap();
}
/**
* Get ip list from cache
*
* @return array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address>
*/
public static function getCacheList(): array
{
try {
/** @var array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address>|null $ip_list */
$ip_list = Cache::get(self::$cache_key);
if (!$ip_list) {
$ip_list = self::fetchIpList();
self::validateIpList($ip_list);
self::setCacheList($ip_list);
return $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
{
Cache::put(self::$cache_key, $ip_list, \DateInterval::createFromDateString(self::$cache_ttl));
}
/**
* Fetch Ip Adress list from Uwil Web
*
* @return array<\Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address>
* @throws \Uwil\AdminSecure\Laravel\Exceptions\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 = self::getOptions()->getEndpoint();
$key = self::getOptions()->getApiKey();
$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;
}
/**
* Check if ipV4 is private
*
@ -187,34 +110,6 @@ abstract class AdminSecure
$initial || $ipv6_range->containsAddress($ipv6_address));
}
/**
* Validate ip list
*
* @param array $ip_list
* @return void
* @throws \Uwil\AdminSecure\Laravel\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\IPv4Address|\Dxw\CIDR\IPv6Address>
*/
private static function parseAdresses(Response $response): array
{
return \collect($response->json())
->map(fn (string $ip) => IPAddress::Make($ip)->unwrap())->all();
}
/**
* Get Admin secure options
*

View file

@ -2,11 +2,9 @@
namespace Uwil\AdminSecure\Laravel;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Illuminate\Routing\Router;
use Uwil\AdminSecure\Laravel\Console\SyncFromLdcWeb;
use Uwil\AdminSecure\Laravel\Http\Middleware\AdminSecureMiddleware;
class AdminSecureServiceProvider extends ServiceProvider
@ -22,21 +20,9 @@ class AdminSecureServiceProvider extends ServiceProvider
__DIR__ . '/../config' => config_path(),
], 'config');
// * 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', 'staging'));
// * Cron Report everyday
$this->app->booted(function () {
if ($this->app->make('hasToFilterIp')) {
/** @var \Illuminate\Console\Scheduling\Schedule */
$schedule = $this->app->make(Schedule::class);
$schedule->command('uwil:admin-secure:sync')->daily();
}
});
/** @var \Illuminate\Routing\Router */
$router = $this->app->make(Router::class);
$router->aliasMiddleware('admin-secure', AdminSecureMiddleware::class);

View file

@ -1,43 +0,0 @@
<?php
namespace Uwil\AdminSecure\Laravel\Console;
use Illuminate\Console\Command;
use Uwil\AdminSecure\Laravel\AdminSecure;
/**
* Send Report Status To LDC Web.
*
* @author Vincent BRIDIER <vincent.bridier@ldc.fr>
*/
class SyncFromLdcWeb extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'uwil:admin-secure:sync';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sync IP list from LDC Web';
/**
* Execute the console command.
*
* @return integer
* @throws \Uwil\AdminSecure\Laravel\Exceptions\AdminSecureException If missing LDC_WEB_API_KEY key.
*/
public function handle(): int
{
$this->info($this->description);
AdminSecure::setCacheList(AdminSecure::fetchIpList());
$this->info('Done.');
return 0;
}
}

View file

@ -1,7 +0,0 @@
<?php
namespace Uwil\AdminSecure\Laravel\Exceptions;
class AdminSecureInvalidAdressException extends AdminSecureException
{
}

View file

@ -4,9 +4,7 @@ namespace Uwil\AdminSecure\Laravel\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 Uwil\AdminSecure\Laravel\AdminSecure;
@ -27,9 +25,10 @@ class AdminSecureMiddleware
}
$client_ip = AdminSecure::fetchClientIp();
$ip_is_allowed = \collect(AdminSecure::getCacheList())
->reduce(fn (null|bool $initial, IPv4Address|IPv6Address $ip_address) => $initial || IP::contains(
$ip_address,
$ip_is_allowed = \collect(AdminSecure::getOptions()->getCidrList())
->reduce(fn (null|bool $initial, IPv4Range|IPv6Range $ip_range) => $initial || IP::contains(
$ip_range,
$client_ip
)->unwrap());

View file

@ -30,7 +30,7 @@ final class Options
*
* @var string
*/
private static string $cache_config_key = 'ldcweb_ip_cidr_config_list';
private static string $cache_config_key = 'uwil_ip_cidr_config_list';
/**
* Class constructor.
@ -47,33 +47,23 @@ final class Options
}
/**
* Get end point url
* Get cidr list
*
* @return string
* @return array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range>
*/
public function getEndpoint(): string
public function getCidrList(): array
{
return $this->options['endpoint'];
return $this->options['cidrList'];
}
/**
* Get api key
*
* @return string
*/
public function getApiKey(): string
{
return $this->options['apiKey'];
}
/**
* Get cidr allowed list
* Get cidr project allowed list
*
* @return array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range>
*/
public function getCidrAllowedList(): array
{
return $this->options['cidrManualAllowedList'];
return $this->options['cidrProjectAllowedList'];
}
/**
@ -85,34 +75,19 @@ final class Options
private function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'endpoint' => '',
'apiKey' => '',
'cidrManualAllowedList' => []
'cidrList' => [],
'cidrProjectAllowedList' => []
]);
$resolver->setAllowedTypes('endpoint', [
'string', \Closure::fromCallable([$this, 'validateEndpoint'])
]);
$resolver->setAllowedTypes('apiKey', 'string');
$resolver->setNormalizer('cidrManualAllowedList', \Closure::fromCallable([$this, 'normalizeCidrList']));
$resolver->setNormalizer('cidrList', \Closure::fromCallable([$this, 'normalizeCidrList']));
$resolver->setNormalizer('cidrProjectAllowedList', \Closure::fromCallable([$this, 'normalizeCidrProjectList']));
}
/**
* Validate endpoint
*
* @param null|string $value
* @return boolean
*/
private function validateEndpoint(?string $value): bool
{
return !(!$value or !\filter_var($value, \FILTER_VALIDATE_URL));
}
/**
* Normalise Cidr list
* Normalise Cidr list for cidrList option
*
* @param \Symfony\Component\OptionsResolver\Options $options
* @param string $value
* @param string|null $value
* @return array
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
*/
@ -121,31 +96,60 @@ final class Options
?string $value
): array {
// phpcs:enable
return $this->doNormalizeCidrList($value, 'cidrList');
}
$allowed_cidr_config_list = collect(explode(',', strval($value)))->filter();
/**
* 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(','));
/** @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 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::forget(self::$cache_config_key);
// 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 AdminSecureException(\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(self::$cache_config_key, fn () => [
return \collect(Cache::rememberForever($cacheConfigKey, fn() => [
'list' => $allowed_cidr_list,
'hash' => $allowed_cidr_config_list_hash
]))->get('list');