Compare commits

...
Sign in to create a new pull request.

30 commits

Author SHA1 Message Date
Jérémy
d0c4dbc6f0 Merge branch 'update' into 'main'
chore: 🔖 release v6.0.1

See merge request ldc-web/projets/ldc-web-admin-secure-laravel!9
2026-10-05 14:46:21 +02:00
Alexis Gousseau
6f687a8cd4
chore: 🔖 release v6.0.1 2026-10-05 14:34:17 +02:00
Alexis Gousseau
71453c6a8c
chore: ⬆️ bump dependencies 2026-10-05 14:33:49 +02:00
Noix
cb88b7ddb5 Merge branch 'update' into 'main'
v6.0.0

See merge request ldc-web/projets/ldc-web-admin-secure-laravel!8
2026-06-24 10:35:38 +00:00
Alexis Gousseau
c67e0b476f
chore: 🔖 release v6.0.0 2026-06-22 12:39:48 +02:00
Alexis Gousseau
cd6bb56bb6
perf: 🚨 resolve linters errors 2026-06-22 12:39:37 +02:00
Alexis Gousseau
ddb4c688dc
chore: 🔧 rename package into laravel-admin-secure 2026-06-22 12:00:55 +02:00
Alexis Gousseau
c2eb783716
chore: 🔧 update vscode settings (colors, intelephense) 2026-06-22 11:46:57 +02:00
Alexis Gousseau
92dbff8b1c
chore: 🔧 update list of files/folder to ignore in git 2026-06-22 11:46:07 +02:00
Alexis Gousseau
1a4f23106f
chore: 🔧 update editor configuration 2026-06-22 11:45:36 +02:00
Alexis Gousseau
facbea062e
chore: 🔧 add gitlab issue templates 2026-06-22 11:44:56 +02:00
Alexis Gousseau
267cec076b
chore: 🔧 update linters configuration 2026-06-22 11:44:24 +02:00
Alexis Gousseau
594f03f834
feat: ✨ add commit lint from husky configuration 2026-06-22 11:42:41 +02:00
Noah Lancien
c54dad85ee fix: 🐛 allow symfony/options-resolver:^8.0 2026-04-27 15:17:14 +02:00
Noix
0299d83971 Merge branch 'laravel-13-compatibility' into 'main'
chore: ⬆️ Added Laravel 13 compatibility

See merge request ldc-web/projets/ldc-web-admin-secure-laravel!7
2026-04-17 15:11:06 +00:00
Noah Lancien
6bed78da14 chore: ⬆️ Added Laravel 13 compatibility 2026-04-17 17:04:22 +02:00
Vincent BRIDIER
f376e1dd4b Merge branch 'update-k8s' into 'main'
feat: ✨ Remove LDC Web API dependency

See merge request ldc-web/projets/ldc-web-admin-secure-laravel!6
2026-01-09 14:25:53 +00:00
Vincent BRIDIER
a4e0fafa16 feat: ✨ Remove LDC Web API dependency 2026-01-09 15:24:46 +01:00
Vincent BRIDIER
26f818b6b9 Merge branch 'laravel-12' into 'main'
feat: ✨ add laravel 12 compatibility

See merge request ldc-web/projets/ldc-web-admin-secure-laravel!5
2025-04-07 11:07:16 +00:00
Alexis Gousseau
4afdddde91 chore: 🔖 release v4.0.0 2025-04-07 12:59:50 +02:00
Alexis Gousseau
e5056174bc feat: ✨ add laravel 12 compatibility 2025-04-07 12:48:01 +02:00
Vincent BRIDIER
61efdb6099
chore: 💥📦⬆️ Bump PHP to 8.3 + Add Laravel 11 compatibility + Change namespace for uwil compliance 2024-09-03 15:49:01 +02:00
Jérémy Munsch
5cf7ba3b45
Update change log to 2.0.3 2024-04-04 15:11:06 +02:00
Jérémy Munsch
572d90b1c5
add staging to hasToFilterIp condition 2024-04-04 15:10:00 +02:00
Jérémy Munsch
f142732754
phpcs and php stan ok 2024-04-04 15:08:22 +02:00
Jérémy Munsch
625685de1a
Merge branch 'develop' 2023-09-19 11:20:29 +02:00
Jérémy Munsch
b05ed93c2b
Merge branch 'develop' 2023-09-19 10:59:32 +02:00
Vincent BRIDIER
5f0c6b136f
Merge branch 'develop' 2023-06-05 12:34:45 +02:00
Vincent BRIDIER
c538f5d349
Merge branch 'trunk' 2022-07-04 11:42:10 +02:00
Vincent BRIDIER
0b398200c8
Merge branch 'develop' into trunk 2022-07-04 11:41:52 +02:00
28 changed files with 12121 additions and 536 deletions

View file

@ -3,16 +3,22 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
insert_final_newline = true indent_size = 4
indent_style = space indent_style = space
indent_size = 4 insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.json]
indent_size = 4
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false
[*.{yml,yaml}] [*.{yml,yaml}]
indent_size = 2 indent_size = 2
[docker-compose.yml]
indent_size = 4
[*.scss]
indent_size = 2
[*.{tsx,ts,cjs}]
indent_size = 2

11
.gitignore vendored
View file

@ -1,7 +1,4 @@
workspace.code-workspace vendor/
vendor node_modules/
.phpunit.result.cache /auth.json
.phpunit.cache /.npmrc
.vscode/launch.json
composer.lock
build

View file

@ -0,0 +1,11 @@
## Comportement constaté
## Comportement attendu
## Solution envisagée
## Étapes pour la reproduction du bug
*

View file

@ -0,0 +1,7 @@
## Présentation du besoin
## Implémentation
## Maquettes, documentations...

3
.husky/commit-msg Normal file
View file

@ -0,0 +1,3 @@
#!/bin/sh
npx --no -- commitlint --config node_modules/@uwil/husky-config/commitlint.config.cjs --edit $1

25
.vscode/settings.json vendored
View file

@ -1,3 +1,26 @@
{ {
"phpcs.executablePath": "vendor/bin/phpcs" "workbench.colorCustomizations": {
"activityBar.activeBackground": "#00d6d6",
"activityBar.activeBorder": "#1a1d02",
"activityBar.background": "#00d6d6",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#df00df",
"activityBarBadge.foreground": "#e7e7e7",
"sash.hoverBorder": "#00d6d6",
"statusBar.background": "#00a3a3",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#00d6d6",
"statusBarItem.remoteBackground": "#00a3a3",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#00a3a3",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#00a3a399",
"titleBar.inactiveForeground": "#e7e7e799",
"commandCenter.border": "#e7e7e799"
},
"peacock.remoteColor": "#00a3a3",
"intelephense.environment.phpVersion": "8.3.0",
"intelephense.completion.fullyQualifyGlobalConstantsAndFunctions": true,
"intelephense.phpdoc.useFullyQualifiedNames": true
} }

101
CHANGELOG.md Normal file
View file

@ -0,0 +1,101 @@
# Changelog
All notable changes to `laravel-admin-secure` will be documented in this file.
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- 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/).
## **[v6.0.1] - 05/10/2026**
### Changed
- chore: ⬆️ bump dependencies
## **[v6.0.0] - 22/06/2026**
### Added
- feat: ✨ add commit lint from husky configuration
### Changed
- chore: 🔧 update linters configuration
- chore: 🔧 rename package into laravel-admin-secure
### Fixed
- perf: 🚨 resolve linters errors
## **[v5.1.0] - 17/04/2026**
### Added
- chore: :arrow_up: Added Laravel 13 compatibility
## **[v5.0.0] - 09/01/2026**
### Added
- feat: ✨ Remove LDC Web API dependency
## **[v4.0.0] - 07/04/2025**
### Added
- feat: ✨ add Laravel 12 compatibility
## **[v3.0.0] - 03/09/2024**
### Added
- Add Laravel 11 compatibility
- Bump deps for Laravel 11
### Changed
- Change namespace for UWIL complience
## **[v2.0.3] - 04/04/2024**
### Added
- Add staging to environnements where sync command should be executed
### Changed
- PHPCS and PHPSTAN fixes
## **[v2.0.2] - 19/09/2024**
### Changed
- fixed artisan command to fetch list
## **[v2.0.1] - 19/09/2023**
### Changed
- Delete API key
- Artisan config cache fix
- Cache was ignored
## **[v2.0.0] - 05/06/2023**
### Added
- Bump laravel 10
- Add manual Cird possibility
### Changed
- Allows connections from private ip ranges
- Add posibility to config manual CIDR exceptions
## **[v1.0.1] - 04/07/2022**
- Run scheduled only on production envirronement
## **[v1.0.0] - 24/06/2022**
- Initial release

7
MIGRATION.md Normal file
View file

@ -0,0 +1,7 @@
# Migration
All notable changes to `laravel-admin-secure` will be documented in this file.
## v5.1.1 -> v6.0.0
- renommage du package de `ldc-web-admin-secure-laravel` en `laravel-admin-secure`,
- changement du namespace pour le middleware de `Uwil\AdminSecure\Laravel\Http\Middleware` à `Uwil\LaravelAdminSecure\Middlewares`.

View file

@ -1,35 +1,59 @@
# LDC Web Admin Secure Laravel # UWIL Laravel Admin Secure
## Install on Laravel UWIL Laravel Admin Secure est un package à installer afin de protéger nos interfaces d'administration à une liste d'IPs spécifiques.
```
composer req ldc/web-admin-secure-laravel
```
.env.example :
```dotenv ## Table des matières
# ADMIN SECURE
# - API ENDPOINT (Optional) - [UWIL Laravel Admin Secure](#uwil-laravel-admin-secure)
LDC_WEB_API_ENDPOINT="https://ldc-web.ldc.fr/api/ldc-networks" - [Table des matières](#table-des-matières)
# - API KEY - [Compatibilité](#Compatibilité)
LDC_WEB_API_KEY="" - [Documentation](#documentation)
- [Installation](#installation)
- [Détails](#détails)
- [Changelog](#changelog)
- [Copyright et licence](#copyright-et-licence)
## Compatibilité
| Version | [1.0.0](https://lab.s-web.fr/ldc-web/projets/laravel-admin-secure/-/tree/1.0.0?ref_type=tags) -> [1.0.1](https://lab.s-web.fr/ldc-web/projets/laravel-admin-secure/-/tree/1.0.1?ref_type=tags) | [2.0.0](https://lab.s-web.fr/ldc-web/projets/laravel-admin-secure/-/tree/2.0.0?ref_type=tags) -> [2.0.3](https://lab.s-web.fr/ldc-web/projets/laravel-admin-secure/-/tree/2.0.3?ref_type=tags) | [3.0.0](https://lab.s-web.fr/ldc-web/projets/laravel-admin-secure/-/tree/3.0.0?ref_type=tags) | [4.0.0](https://lab.s-web.fr/ldc-web/projets/laravel-admin-secure/-/tree/4.0.0?ref_type=tags) -> [5.0.0](https://lab.s-web.fr/ldc-web/projets/laravel-admin-secure/-/tree/5.0.0?ref_type=tags) | [5.1.0](https://lab.s-web.fr/ldc-web/projets/laravel-admin-secure/-/tree/5.1.0?ref_type=tags) -> [6.0.1](https://lab.s-web.fr/ldc-web/projets/laravel-admin-secure/-/tree/6.0.1?ref_type=tags) |
|:-------------------------------------------------------------------------|:--:|:--:|:--:|:--:|:--:|
| [laravel\framework^13.0](https://github.com/laravel/framework/tree/13.x) | ❌ | ❌ | ❌ | ❌ | ✅ |
| [laravel\framework^12.0](https://github.com/laravel/framework/tree/12.x) | ❌ | ❌ | ❌ | ✅ | ✅ |
| [laravel\framework^11.0](https://github.com/laravel/framework/tree/11.x) | ❌ | ❌ | ✅ | ✅ | ✅ |
| [laravel\framework^10.0](https://github.com/laravel/framework/tree/10.x) | ❌ | ✅ | ❌ | ❌ | ❌ |
| [laravel\framework^9.0](https://github.com/laravel/framework/tree/10.x) | ✅ | ❌ | ❌ | ❌ | ❌ |
| [laravel\framework^8.0](https://github.com/laravel/framework/tree/10.x) | ✅ | ❌ | ❌ | ❌ | ❌ |
## Documentation
### Installation
1. Installer ce package composer comme ceci:
```bash
composer req uwil/laravel-admin-secure
```
2. Ajouter simplement l'alias du middleware `admin-secure` sur les routes que vous voulez protéger,
2. Enfin, renseigner la liste des IPs dans votre `.env`:
```bash
# UWIL ADMIN SECURE
# - LIST SEPARATED WITH ',' (Required)
UWIL_ADMIN_SECURE_CIDR_LIST=""
# - LIST SEPARATED WITH ',' (Optional) # - LIST SEPARATED WITH ',' (Optional)
LDC_WEB_CIDR_MANUAL_ALLOWED_LIST="" UWIL_ADMIN_SECURE_CIDR_PROJECT_ALLOWED_LIST=""
``` ```
README.md : ## Changelog
```
Put every min in PROD :
php artisan schedule:run
```
## Available Commands Dernière version v6.0.1.
```
ldcweb: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. Voir le journal de modifications [CHANGELOG.md](CHANGELOG.md) pour les détails.
You just need to install this package.
**!! Runs only in production !!** ## Copyright et licence
Laravel Admin Secure est écrit et maintenu par UWIL.
Copyright (c) 2022 et au-delà UWIL.

View file

@ -1,31 +0,0 @@
# Changelog
## **Version 2.0.2**
### *2.0.2 Fixed*
- fixed artisan command to fetch list
## **Version 2.0.1**
### *2.0.1 Fixed*
- artisan config cache fix
- Cache was ignored
## **Version 2.0.0**
### *2.0.0 Changed*
- Bump laravel 10
- Add manual Cird possibility
## **Version 1.0.1**
### *1.0.1 Changed*
- Run scheduled only on production envirronement
## **Version 1.0.0**
### Initial release

1
commitlint.config.cjs Normal file
View file

@ -0,0 +1 @@
module.exports = require('@uwil/husky-config/commitlint.config.cjs');

View file

@ -1,50 +1,64 @@
{ {
"name": "ldc/web-admin-secure-laravel", "name": "uwil/laravel-admin-secure",
"description": "Restrict access to LDC Network", "description": "Restrict access to LDC Network",
"type": "library",
"license": "MIT", "license": "MIT",
"authors": [ "version": "6.0.1",
{ "keywords": [
"name": "Vincent BRIDIER", "uwil",
"email": "vincent.bridier@ldc.fr", "laravel",
"homepage": "https://www.ldc.fr" "security"
}
], ],
"readme": "https://lab.s-web.fr/ldc-web/projets/laravel-admin-secure/-/blob/main/README.md",
"support": {
"email": "digital@ldc.fr",
"issues": "https://lab.s-web.fr/ldc-web/projets/laravel-admin-secure/-/issues"
},
"require": { "require": {
"php": ">=8.2", "php": "^8.3",
"illuminate/support": "~10", "illuminate/support": "~11|~12|~13",
"laravel/framework": "~10", "laravel/framework": "~11|~12|~13",
"symfony/options-resolver": "^3.4.43|^4.4.30|^5.0.11|^6.0", "symfony/options-resolver": "^7.0|^8.0",
"dxw/cidr": "^3.1" "dxw/cidr": "^4.0.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9", "driftingly/rector-laravel": "^2.3",
"squizlabs/php_codesniffer": "^3", "larastan/larastan": "^3.9",
"orchestra/testbench": "^8", "laravel/pint": "^1.29",
"slevomat/coding-standard": "^7", "orchestra/testbench": "^11.1",
"phpunit/php-code-coverage": "^9" "slevomat/coding-standard": "^8.3",
}, "squizlabs/php_codesniffer": "^3.7"
"autoload": {
"psr-4": {
"LDC\\WebAdminSecureLaravel\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"LDC\\WebAdminSecureLaravel\\Tests\\": "tests"
}
},
"extra": {
"laravel": {
"providers": [
"LDC\\WebAdminSecureLaravel\\AdminSecureServiceProvider"
]
}
}, },
"scripts": { "scripts": {
"post-autoload-dump": [ "post-autoload-dump": [
"@php ./vendor/bin/testbench package:discover --ansi" "@php ./vendor/bin/testbench package:discover --ansi"
], ],
"test": "vendor/bin/phpunit" "phpcs": [
"./vendor/bin/phpcs"
],
"phpstan": [
"./vendor/bin/phpstan"
],
"rector": [
"./vendor/bin/rector"
]
},
"autoload": {
"psr-4": {
"Uwil\\LaravelAdminSecure\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Uwil\\LaravelAdminSecure\\Tests\\": "tests"
}
},
"extra": {
"laravel": {
"providers": [
"Uwil\\LaravelAdminSecure\\AdminSecureServiceProvider"
]
}
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {

10198
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,6 +1,6 @@
MIT License 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

1324
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

8
package.json Normal file
View file

@ -0,0 +1,8 @@
{
"name": "laravel-admin-secure",
"version": "6.0.1",
"description": "UWIL K8S utils",
"dependencies": {
"@uwil/husky-config": "^1.0.1"
}
}

144
phpcs.xml
View file

@ -1,122 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<ruleset name="RealWorld" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd"> <ruleset name="Php Coding Standard"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">
<description>Optimized standard: Static analysis complementary to Pint.</description>
<arg name="colors" /> <arg name="colors" />
<arg name="extensions" value="php" /> <arg name="extensions" value="php" />
<arg name="parallel" value="75" />
<file>src/</file> <file>src/</file>
<rule ref="PSR1" /> <file>config/</file>
<rule ref="PSR12" />
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/storage/*</exclude-pattern>
<exclude-pattern>*/packages/*</exclude-pattern>
<exclude-pattern type="relative">*.blade.php</exclude-pattern>
<rule ref="PSR12"/>
<rule ref="Generic.Classes.DuplicateClassName"/>
<rule ref="Generic.CodeAnalysis.EmptyStatement"/> <rule ref="Generic.CodeAnalysis.EmptyStatement"/>
<rule ref="Generic.CodeAnalysis.ForLoopShouldBeWhileLoop"/>
<rule ref="Generic.CodeAnalysis.ForLoopWithTestFunctionCall"/>
<rule ref="Generic.CodeAnalysis.JumbledIncrementer"/>
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/> <rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
<rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
<rule ref="Generic.CodeAnalysis.UnusedFunctionParameter"/>
<rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/> <rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
<rule ref="Generic.Commenting.DocComment.SpacingBeforeTags"/> <rule ref="Generic.Metrics.CyclomaticComplexity">
<rule ref="Generic.Commenting.Todo"/> <properties>
<rule ref="Generic.Commenting.Fixme"/> <property name="complexity" value="10"/>
<rule ref="Generic.ControlStructures.InlineControlStructure"/> <property name="absoluteComplexity" value="20"/>
<rule ref="Generic.Files.ByteOrderMark"/> </properties>
<rule ref="Generic.Files.LineEndings"/> </rule>
<rule ref="Generic.Files.LineLength"/>
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
<rule ref="Generic.Formatting.MultipleStatementAlignment"/>
<rule ref="Generic.Formatting.NoSpaceAfterCast"/>
<rule ref="Generic.Functions.CallTimePassByReference"/>
<rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
<rule ref="Generic.Metrics.CyclomaticComplexity"/>
<rule ref="Generic.Metrics.NestingLevel"/> <rule ref="Generic.Metrics.NestingLevel"/>
<rule ref="Generic.NamingConventions.ConstructorName"/>
<rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
<rule ref="Generic.NamingConventions.CamelCapsFunctionName"/>
<rule ref="Generic.PHP.DeprecatedFunctions"/> <rule ref="Generic.PHP.DeprecatedFunctions"/>
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
<rule ref="Generic.PHP.ForbiddenFunctions"/> <rule ref="Generic.PHP.ForbiddenFunctions"/>
<rule ref="Generic.PHP.NoSilencedErrors"/> <rule ref="Generic.PHP.NoSilencedErrors"/>
<rule ref="Generic.Strings.UnnecessaryStringConcat"/>
<rule ref="Generic.WhiteSpace.DisallowTabIndent"/>
<rule ref="Generic.WhiteSpace.ScopeIndent"/>
<rule ref="MySource.PHP.EvalObjectFactory"/>
<rule ref="MySource.PHP.GetRequestData"/>
<rule ref="MySource.PHP.AjaxNullComparison"/>
<rule ref="PEAR.Classes.ClassDeclaration"/>
<rule ref="PEAR.Commenting.InlineComment"/>
<rule ref="PSR1.Classes.ClassDeclaration"/>
<rule ref="PSR1.Files.SideEffects"/>
<rule ref="PSR2.Classes.ClassDeclaration"/>
<rule ref="PSR2.Classes.PropertyDeclaration"/>
<rule ref="PSR2.ControlStructures.ElseIfDeclaration"/>
<rule ref="PSR2.ControlStructures.SwitchDeclaration"/>
<rule ref="PSR2.Files.EndFileNewline"/>
<rule ref="PSR2.Methods.MethodDeclaration"/>
<rule ref="PSR2.Namespaces.NamespaceDeclaration"/>
<rule ref="PSR2.Namespaces.UseDeclaration"/>
<rule ref="Squiz.PHP.CommentedOutCode"/>
<rule ref="Squiz.PHP.DisallowMultipleAssignments">
<exclude name="Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure" />
</rule>
<rule ref="Squiz.PHP.DisallowSizeFunctionsInLoops"/>
<rule ref="Squiz.PHP.DiscouragedFunctions">
<properties>
<property name="error" value="true"/>
</properties>
</rule>
<rule ref="Squiz.PHP.Eval"/> <rule ref="Squiz.PHP.Eval"/>
<rule ref="Squiz.PHP.GlobalKeyword"/> <rule ref="Squiz.PHP.GlobalKeyword"/>
<rule ref="Squiz.PHP.Heredoc"/>
<rule ref="Squiz.PHP.InnerFunctions"/>
<rule ref="Squiz.PHP.LowercasePHPFunctions"/>
<rule ref="Squiz.PHP.NonExecutableCode"/> <rule ref="Squiz.PHP.NonExecutableCode"/>
<rule ref="Squiz.Scope.MemberVarScope"/>
<rule ref="Squiz.Scope.MethodScope"/>
<rule ref="Squiz.Scope.StaticThisUsage"/> <rule ref="Squiz.Scope.StaticThisUsage"/>
<rule ref="Squiz.WhiteSpace.LanguageConstructSpacing"/>
<rule ref="Squiz.WhiteSpace.LogicalOperatorSpacing"/>
<rule ref="Squiz.WhiteSpace.MemberVarSpacing">
<properties>
<property name="spacingBeforeFirst " value="false"/>
</properties>
</rule>
<rule ref="Squiz.WhiteSpace.OperatorSpacing">
<properties>
<property name="ignoreNewlines" value="true"/>
</properties>
</rule>
<rule ref="Squiz.WhiteSpace.ObjectOperatorSpacing">
<properties>
<property name="ignoreNewlines" value="true"/>
</properties>
</rule>
<rule ref="Squiz.WhiteSpace.ScopeKeywordSpacing"/>
<rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
<rule ref="Squiz.Commenting.BlockComment">
<exclude name="Squiz.Commenting.BlockComment.WrongStart" />
</rule>
<rule ref="Squiz.Commenting.ClassComment">
<exclude name="Squiz.Commenting.ClassComment.Missing" />
<exclude name="Squiz.Commenting.ClassComment.TagNotAllowed" />
</rule>
<rule ref="Squiz.Commenting.DocCommentAlignment"/>
<rule ref="Squiz.Commenting.EmptyCatchComment"/>
<rule ref="Squiz.Commenting.FunctionComment">
<exclude name="Squiz.Commenting.FunctionComment.MissingParamComment" />
</rule>
<rule ref="Squiz.Commenting.FunctionCommentThrowTag"/>
<rule ref="Squiz.Commenting.InlineComment">
<exclude name="Squiz.Commenting.InlineComment.DocBlock" />
</rule>
<rule ref="Squiz.Commenting.LongConditionClosingComment">
<exclude name="Squiz.Commenting.LongConditionClosingComment.SpacingBefore" />
</rule>
<rule ref="Squiz.Commenting.PostStatementComment"/>
<rule ref="Squiz.Commenting.VariableComment"/>
<rule ref="SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse" />
<!-- For Laravel migrations --> <rule ref="Generic.NamingConventions.ConstructorName"/>
<rule ref="Generic.NamingConventions.CamelCapsFunctionName"/>
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="150"/>
<property name="absoluteLineLimit" value="250"/>
</properties>
</rule>
<rule ref="SlevomatCodingStandard.Namespaces.UnusedUses" />
<rule ref="SlevomatCodingStandard.TypeHints.DeclareStrictTypes">
<properties>
<property name="spacesCountAroundEqualsSign" value="0" />
<property name="declareOnFirstLine" value="false" />
<property name="linesCountBeforeDeclare" value="1" />
<property name="linesCountAfterDeclare" value="1" />
</properties>
</rule>
<rule ref="PSR1.Classes.ClassDeclaration.MissingNamespace"> <rule ref="PSR1.Classes.ClassDeclaration.MissingNamespace">
<exclude-pattern>*/database/migrations/*</exclude-pattern> <exclude-pattern>*/database/migrations/*</exclude-pattern>
</rule> </rule>

14
phpstan.neon Normal file
View file

@ -0,0 +1,14 @@
includes:
- vendor/larastan/larastan/extension.neon
parameters:
paths:
- config/
- src/
level: 7
ignoreErrors:
-
message: '#Called .env. outside of the config directory#'
path: config/*

80
pint.json Normal file
View file

@ -0,0 +1,80 @@
{
"preset": "laravel",
"exclude": [
"packages"
],
"rules": {
"indentation_type": true,
"single_line_empty_body": false,
"types_spaces": {
"space": "single",
"space_multiple_catch": "single"
},
"control_structure_braces": true,
"new_with_parentheses": {
"anonymous_class": true,
"named_class": true
},
"class_definition": {
"single_item_single_line": false,
"single_line": false,
"space_before_parenthesis": true
},
"curly_braces_position": {
"allow_single_line_empty_anonymous_classes": false,
"classes_opening_brace": "next_line_unless_newline_at_signature_end",
"anonymous_classes_opening_brace": "next_line_unless_newline_at_signature_end",
"control_structures_opening_brace": "same_line",
"functions_opening_brace": "next_line_unless_newline_at_signature_end",
"anonymous_functions_opening_brace": "same_line"
},
"binary_operator_spaces": {
"default": "align_single_space_minimal",
"operators": {
"=": "align_single_space",
"|": "single_space",
"=>": "align_single_space",
".=": "align_single_space"
}
},
"concat_space": {
"spacing": "one"
},
"align_multiline_comment": {
"comment_type": "all_multiline"
},
"array_indentation": true,
"method_chaining_indentation": true,
"statement_indentation": {
"stick_comment_to_next_continuous_control_statement": true
},
"no_unused_imports": true,
"fully_qualified_strict_types": {
"import_symbols": true,
"leading_backslash_in_global_namespace": true,
"phpdoc_tags": []
},
"visibility_required": {
"elements": [
"const",
"method",
"property"
]
},
"no_useless_else": true,
"function_declaration": {
"closure_function_spacing": "one",
"closure_fn_spacing": "one",
"trailing_comma_single_line": false
},
"single_line_after_imports": true,
"combine_consecutive_issets": true,
"array_syntax": {
"syntax": "short"
},
"single_quote": true,
"blank_line_after_namespace": true,
"blank_line_after_opening_tag": true,
"declare_strict_types": true
}
}

52
rector.php Normal file
View file

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use RectorLaravel\Rector\Class_\CommandHiddenPropertyToHiddenAttributeRector;
use RectorLaravel\Rector\Class_\DescriptionPropertyToDescriptionAttributeRector;
use RectorLaravel\Rector\Class_\FillablePropertyToFillableAttributeRector;
use RectorLaravel\Rector\Class_\HiddenPropertyToHiddenAttributeRector;
use RectorLaravel\Rector\Class_\ModelCastsPropertyToCastsMethodRector;
use RectorLaravel\Rector\Class_\SignaturePropertyToSignatureAttributeRector;
use RectorLaravel\Rector\Class_\TablePropertyToTableAttributeRector;
use RectorLaravel\Rector\Class_\WithoutTimestampsPropertyToWithoutTimestampsAttributeRector;
use RectorLaravel\Rector\MethodCall\ValidationRuleArrayStringValueToArrayRector;
use RectorLaravel\Set\LaravelLevelSetList;
use RectorLaravel\Set\LaravelSetList;
use RectorLaravel\Set\LaravelSetProvider;
return RectorConfig::configure()
->withSetProviders(LaravelSetProvider::class)
->withPaths([
__DIR__ . '/config',
__DIR__ . '/src',
])
->withPhpSets()
->withComposerBased(laravel: true)
->withSets([
LaravelLevelSetList::UP_TO_LARAVEL_130,
LaravelSetList::LARAVEL_ARRAYACCESS_TO_METHOD_CALL,
LaravelSetList::LARAVEL_CODE_QUALITY,
LaravelSetList::LARAVEL_COLLECTION,
LaravelSetList::LARAVEL_IF_HELPERS,
LaravelSetList::LARAVEL_LEGACY_FACTORIES_TO_CLASSES,
LaravelSetList::LARAVEL_TYPE_DECLARATIONS,
])
->withTypeCoverageLevel(0)
->withDeadCodeLevel(0)
->withCodeQualityLevel(0)
->withSkip([
// Disable Laravel notations.
CommandHiddenPropertyToHiddenAttributeRector::class,
DescriptionPropertyToDescriptionAttributeRector::class,
FillablePropertyToFillableAttributeRector::class,
HiddenPropertyToHiddenAttributeRector::class,
ModelCastsPropertyToCastsMethodRector::class,
SignaturePropertyToSignatureAttributeRector::class,
TablePropertyToTableAttributeRector::class,
WithoutTimestampsPropertyToWithoutTimestampsAttributeRector::class,
// Validation.
ValidationRuleArrayStringValueToArrayRector::class,
]);

View file

@ -1,230 +1,102 @@
<?php <?php
namespace LDC\WebAdminSecureLaravel; declare(strict_types=1);
namespace Uwil\LaravelAdminSecure;
use Dxw\CIDR\IPAddress; use Dxw\CIDR\IPAddress;
use Dxw\CIDR\IPv4Address; use Dxw\CIDR\IPv4Address;
use Dxw\CIDR\IPv4Range; use Dxw\CIDR\IPv4Range;
use Dxw\CIDR\IPv6Address; use Dxw\CIDR\IPv6Address;
use Dxw\CIDR\IPv6Range; use Dxw\CIDR\IPv6Range;
use Illuminate\Http\Client\Response; use Uwil\LaravelAdminSecure\Exceptions\AdminSecureException;
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 abstract class AdminSecure
{ {
/** /** @var array<int,string> Private ipV4 CIDR list. */
* 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
*
* @var array
*/
private static array $private_ip_v4_cidr_list = [ private static array $private_ip_v4_cidr_list = [
'10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16' '10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
]; ];
/** /** @var string Private ipV4 CIDR loopback. */
* Private ipV4 CIDR loopback
*
* @var string
*/
private static string $private_ip_v4_loopback_cidr = '127.0.0.0/8'; private static string $private_ip_v4_loopback_cidr = '127.0.0.0/8';
/** /** @var array<int,string> Private ipV6 CIDR list. */
* Private ipV6 CIDR list
*
* @var array
*/
private static array $private_ip_v6_cidr_list = [ private static array $private_ip_v6_cidr_list = [
'fd00::/8' 'fd00::/8',
]; ];
/** /**
* Private ipV6 CIDR loopback * Private ipV6 CIDR loopback
*
* @var string
*/ */
private static string $private_ip_v6_loopback_cidr = '::1/128'; private static string $private_ip_v6_loopback_cidr = '::1/128';
/** /** @var \Uwil\LaravelAdminSecure\Options|null Private options. */
* Private options
*
* @var \LDC\WebAdminSecureLaravelOptions|null
*/
private static $options = null; private static $options = null;
/** /**
* Check if ip is private * 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 public static function ipIsPrivate(IPv4Address | IPv6Address $ip_address): bool
{ {
return \get_class($ip_address) === IPv4Address::class ? return $ip_address instanceof IPv4Address ?
self::ipV4IsPrivate($ip_address) : self::ipV6IsPrivate($ip_address); self::ipV4IsPrivate($ip_address) : self::ipV6IsPrivate($ip_address);
} }
/** /**
* Fetch client IP * Fetch client IP.
* *
* @return \Dxw\CIDR\IPv4Address|\Dxw\CIDR\IPv6Address * @throws \Uwil\LaravelAdminSecure\Exceptions\AdminSecureException If the ip is invalid.
* @throws \LDC\WebAdminSecureLaravel\AdminSecureException If the ip is invalid.
*/ */
public static function fetchClientIp(): IPv4Address|IPv6Address public static function fetchClientIp(): IPv4Address | IPv6Address
{ {
// ! HA PROXY HACK. // ! REVERSE PROXY HACK.
$ip = request()->server('HTTP_X_FORWARDED_FOR') ?? request()->getClientIp(); $forwarded = request()->server('HTTP_X_FORWARDED_FOR');
$ip = IPAddress::Make($ip); $ip = is_string($forwarded) && $forwarded !== ''
? trim(explode(',', $forwarded)[0])
: request()->getClientIp() ?? '0.0.0.0';
$ip = IPAddress::Make($ip);
if ($ip->isErr()) { if ($ip->isErr()) {
throw new AdminSecureException(\trans('Client IP not valid ') . $ip->getErr()); throw new AdminSecureException(\trans('Client IP not valid ') . $ip->getErr());
} }
return $ip->unwrap(); return $ip->unwrap();
} }
/** /**
* Get ip list from cache * Check if ipV4 is private.
*
* @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 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 = 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
*
* @param \Dxw\CIDR\IPv4Address $ipv4_address
* @return boolean
*/ */
private static function ipV4IsPrivate(IPv4Address $ipv4_address): bool private static function ipV4IsPrivate(IPv4Address $ipv4_address): bool
{ {
return \collect(self::$private_ip_v4_cidr_list) return \collect(self::$private_ip_v4_cidr_list)
->concat([self::$private_ip_v4_loopback_cidr]) ->concat([self::$private_ip_v4_loopback_cidr])
->map(fn (string $ipv4_cidr) => IPv4Range::Make($ipv4_cidr)->unwrap()) ->map(fn (string $ipv4_cidr) => IPv4Range::Make($ipv4_cidr)->unwrap())
->reduce(fn (bool|null $initial, IPv4Range $ipv4_range) => ->reduce(fn (?bool $initial, IPv4Range $ipv4_range) => $initial || $ipv4_range->containsAddress($ipv4_address));
$initial || $ipv4_range->containsAddress($ipv4_address));
} }
/** /**
* Check if ipV6 is private * Check if ipV6 is private.
*
* @param \Dxw\CIDR\IPv6Address $ipv6_address
* @return boolean
*/ */
private static function ipV6IsPrivate(IPv6Address $ipv6_address): bool private static function ipV6IsPrivate(IPv6Address $ipv6_address): bool
{ {
return \collect(self::$private_ip_v6_cidr_list) return \collect(self::$private_ip_v6_cidr_list)
->concat([self::$private_ip_v6_loopback_cidr]) ->concat([self::$private_ip_v6_loopback_cidr])
->map(fn (string $ipv6_cidr) => IPv6Range::Make($ipv6_cidr)->unwrap()) ->map(fn (string $ipv6_cidr) => IPv6Range::Make($ipv6_cidr)->unwrap())
->reduce(fn (bool|null $initial, IPv6Range $ipv6_range) => ->reduce(fn (?bool $initial, IPv6Range $ipv6_range) => $initial || $ipv6_range->containsAddress($ipv6_address));
$initial || $ipv6_range->containsAddress($ipv6_address));
} }
/** /**
* Validate ip list * Get Admin secure options.
*
* @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();
}
/**
* Get Admin secure options
*
* @return \LDC\WebAdminSecureLaravel\Options
*/ */
public static function getOptions(): Options public static function getOptions(): Options
{ {
if (!self::$options) { if (! self::$options) {
self::$options = new Options(\config('admin-secure')); self::$options = new Options(\config('admin-secure'));
} }
return self::$options; return self::$options;
} }
} }

View file

@ -1,13 +1,13 @@
<?php <?php
namespace LDC\WebAdminSecureLaravel; declare(strict_types=1);
namespace Uwil\LaravelAdminSecure;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use Illuminate\Routing\Router; use Illuminate\Routing\Router;
use LDC\WebAdminSecureLaravel\Console\SyncFromLDCWeb; use Illuminate\Support\ServiceProvider;
use LDC\WebAdminSecureLaravel\Http\Middleware\AdminSecureMiddleware; use Uwil\LaravelAdminSecure\Middlewares\AdminSecureMiddleware;
class AdminSecureServiceProvider extends ServiceProvider class AdminSecureServiceProvider extends ServiceProvider
{ {
@ -22,20 +22,8 @@ class AdminSecureServiceProvider extends ServiceProvider
__DIR__ . '/../config' => config_path(), __DIR__ . '/../config' => config_path(),
], 'config'); ], 'config');
// * Register the command if we are using the application via the CLI . // * Set filter condition.
$this->commands([SyncFromLDCWeb::class]); $this->app->singleton('hasToFilterIp', fn (Application $app) => $app->environment('production', 'staging'));
// * Set filter condition
$this->app->singleton('hasToFilterIp', fn (Application $app) => $app->environment('production'));
// * 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('ldcweb:admin-secure-sync')->daily();
}
});
/** @var \Illuminate\Routing\Router */ /** @var \Illuminate\Routing\Router */
$router = $this->app->make(Router::class); $router = $this->app->make(Router::class);
@ -47,6 +35,7 @@ class AdminSecureServiceProvider extends ServiceProvider
* *
* @return void * @return void
*/ */
#[\Override]
public function register() public function register()
{ {
$this->mergeConfigFrom( $this->mergeConfigFrom(

View file

@ -1,43 +0,0 @@
<?php
namespace LDC\WebAdminSecureLaravel\Console;
use Illuminate\Console\Command;
use LDC\WebAdminSecureLaravel\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 = 'ldcweb:admin-secure-sync';
/**
* The console command description.
*
* @var string
*/
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(): int
{
$this->info($this->description);
AdminSecure::setCacheList(AdminSecure::fetchIpList());
$this->info('Done.');
return 0;
}
}

View file

@ -1,6 +1,8 @@
<?php <?php
namespace LDC\WebAdminSecureLaravel\Exceptions; declare(strict_types=1);
namespace Uwil\LaravelAdminSecure\Exceptions;
class AdminSecureException extends \Exception class AdminSecureException extends \Exception
{ {

View file

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

View file

@ -1,43 +1,39 @@
<?php <?php
namespace LDC\WebAdminSecureLaravel\Http\Middleware; declare(strict_types=1);
namespace Uwil\LaravelAdminSecure\Middlewares;
use Closure; use Closure;
use Dxw\CIDR\IP;
use Dxw\CIDR\IPv4Address;
use Dxw\CIDR\IPv4Range; use Dxw\CIDR\IPv4Range;
use Dxw\CIDR\IPv6Address;
use Dxw\CIDR\IPv6Range; use Dxw\CIDR\IPv6Range;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use LDC\WebAdminSecureLaravel\AdminSecure; use Uwil\LaravelAdminSecure\AdminSecure;
class AdminSecureMiddleware class AdminSecureMiddleware
{ {
/** /**
* Handle an incoming request. * Handle an incoming request.
* *
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed * @return mixed
*/ */
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
if (!\app()->make('hasToFilterIp')) { if (! \app()->make('hasToFilterIp')) {
return $next($request); return $next($request);
} }
$client_ip = AdminSecure::fetchClientIp(); $client_ip = AdminSecure::fetchClientIp();
$ip_is_allowed = \collect(AdminSecure::getCacheList())
->reduce(fn (bool|null $initial, IPv4Address|IPv6Address $ip_address) => $initial || IP::contains( $ip_is_allowed = \collect(AdminSecure::getOptions()->getCidrList())
$ip_address, ->reduce(fn (?bool $initial, IPv4Range | IPv6Range $ip_range) => $initial || $ip_range->containsAddress(
$client_ip $client_ip
)->unwrap()); ));
$ip_is_allowed_by_config = \collect(AdminSecure::getOptions()->getCidrAllowedList()) $ip_is_allowed_by_config = \collect(AdminSecure::getOptions()->getCidrAllowedList())
->reduce(fn (bool|null $initial, IPv4Range|IPv6Range $ip_range) => $initial || IP::contains( ->reduce(fn (?bool $initial, IPv4Range | IPv6Range $ip_range) => $initial || $ip_range->containsAddress(
$ip_range,
$client_ip $client_ip
)->unwrap()); ));
if ($ip_is_allowed or $ip_is_allowed_by_config or AdminSecure::ipIsPrivate($client_ip)) { if ($ip_is_allowed or $ip_is_allowed_by_config or AdminSecure::ipIsPrivate($client_ip)) {
return $next($request); return $next($request);

View file

@ -2,40 +2,32 @@
declare(strict_types=1); declare(strict_types=1);
namespace LDC\WebAdminSecureLaravel; namespace Uwil\LaravelAdminSecure;
use Dxw\CIDR\IPv4Range; use Dxw\CIDR\IPv4Range;
use Dxw\CIDR\IPv6Range; use Dxw\CIDR\IPv6Range;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use LDC\WebAdminSecureLaravel\Exceptions\AdminSecureException;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Uwil\LaravelAdminSecure\Exceptions\AdminSecureException;
/** /**
* Configuration * Configuration
*/ */
final class Options final class Options
{ {
/** /** @var array<string, mixed> The configuration options. */
* @var array<string, mixed> The configuration options
*/
private $options; private $options;
/** /** @var \Symfony\Component\OptionsResolver\OptionsResolver The options resolver. */
* @var \Symfony\Component\OptionsResolver\OptionsResolver The options resolver
*/
private $resolver; private $resolver;
/** /** @var string The cache key where to store config cidr list. */
* The cache key where to store config cidr list. private static string $cache_config_key = 'uwil_ip_cidr_config_list';
*
* @var string
*/
private static string $cache_config_key = 'ldcweb_ip_cidr_config_list';
/** /**
* Class constructor. * Class constructor.
* *
* @param array<string, mixed> $options The configuration options. * @param array<string, mixed> $options The configuration options.
*/ */
public function __construct(array $options = []) public function __construct(array $options = [])
{ {
@ -47,73 +39,46 @@ 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 * Get cidr project allowed list.
*
* @return string
*/
public function getApiKey(): string
{
return $this->options['apiKey'];
}
/**
* Get cidr allowed list
* *
* @return array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range> * @return array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range>
*/ */
public function getCidrAllowedList(): array public function getCidrAllowedList(): array
{ {
return $this->options['cidrManualAllowedList']; return $this->options['cidrProjectAllowedList'];
} }
/** /**
* Configures the options of the client. * Configures the options of the client.
* *
* @param \Symfony\Component\OptionsResolver\OptionsResolver $resolver The resolver for the options. * @param \Symfony\Component\OptionsResolver\OptionsResolver $resolver The resolver for the options.
* @return void
*/ */
private function configureOptions(OptionsResolver $resolver): void private function configureOptions(OptionsResolver $resolver): void
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'endpoint' => '', 'cidrList' => [],
'apiKey' => '', 'cidrProjectAllowedList' => [],
'cidrManualAllowedList' => []
]); ]);
$resolver->setAllowedTypes('endpoint', [ $resolver->setNormalizer('cidrList', $this->normalizeCidrList(...));
'string', \Closure::fromCallable([$this, 'validateEndpoint']) $resolver->setNormalizer('cidrProjectAllowedList', $this->normalizeCidrProjectList(...));
]);
$resolver->setAllowedTypes('apiKey', 'string');
$resolver->setNormalizer('cidrManualAllowedList', \Closure::fromCallable([$this, 'normalizeCidrList']));
} }
/** /**
* Validate endpoint * Normalise Cidr list for cidrList option.
* *
* @param null|string $value * @return array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range>
* @return boolean
*/
private function validateEndpoint(?string $value): bool
{
return !(!$value or !\filter_var($value, \FILTER_VALIDATE_URL));
}
/**
* Normalise Cidr list
* *
* @param \Symfony\Component\OptionsResolver\Options $options
* @param string $value
* @return array
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
*/ */
private function normalizeCidrList( private function normalizeCidrList(
@ -121,33 +86,64 @@ final class Options
?string $value ?string $value
): array { ): array {
// phpcs:enable // phpcs:enable
return $this->doNormalizeCidrList($value, 'cidrList');
}
/**
* Normalise Cidr list for cidrProjectAllowedList option.
*
* @return array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range>
*
* @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.
*
* @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 = collect(explode(',', strval($value)))->filter();
$allowed_cidr_config_list_hash = \md5($allowed_cidr_config_list->implode(',')); $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 /** @var array{list: array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range>, hash: string}|null $cached */
if ($allowed_cidr_config_list_hash !== \collect(Cache::get(self::$cache_config_key))->get('hash')) { $cached = Cache::get($cacheConfigKey);
Cache::flush(self::$cache_config_key);
// Flush cache if config hash changed
if ($cached === null || $cached['hash'] !== $allowed_cidr_config_list_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();
Cache::forever($cacheConfigKey, [
'list' => $allowed_cidr_list,
'hash' => $allowed_cidr_config_list_hash,
]);
return $allowed_cidr_list;
} }
/** @var array<\Dxw\CIDR\IPv4Range|\Dxw\CIDR\IPv6Range> $allowed_cidr_list */ return $cached['list'];
return \collect(Cache::rememberForever(self::$cache_config_key, fn () => [
'list' => $allowed_cidr_list,
'hash' => $allowed_cidr_config_list_hash
]))->get('list');
} }
} }