fix(database): skip credential pattern validation for unchanged values
Pattern enforcement now conditional on field being dirty (changed vs saved value). Prevents false validation failures when existing records hold legacy credential formats that pre-date the stricter regex rules.
This commit is contained in:
parent
03313e54cc
commit
40a9881ef2
10 changed files with 165 additions and 24 deletions
|
|
@ -76,8 +76,12 @@ protected function rules(): array
|
|||
return [
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'clickhouseAdminUser' => ValidationPatterns::databaseIdentifierRules(),
|
||||
'clickhouseAdminPassword' => ValidationPatterns::databasePasswordRules(),
|
||||
'clickhouseAdminUser' => ValidationPatterns::databaseIdentifierRules(
|
||||
enforcePattern: $this->clickhouseAdminUser !== $this->database->clickhouse_admin_user,
|
||||
),
|
||||
'clickhouseAdminPassword' => ValidationPatterns::databasePasswordRules(
|
||||
enforcePattern: $this->clickhouseAdminPassword !== $this->database->clickhouse_admin_password,
|
||||
),
|
||||
'image' => 'required|string',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'isPublic' => 'nullable|boolean',
|
||||
|
|
|
|||
|
|
@ -89,7 +89,9 @@ protected function rules(): array
|
|||
return [
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'dragonflyPassword' => ValidationPatterns::databasePasswordRules(),
|
||||
'dragonflyPassword' => ValidationPatterns::databasePasswordRules(
|
||||
enforcePattern: $this->dragonflyPassword !== $this->database->dragonfly_password,
|
||||
),
|
||||
'image' => 'required|string',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'isPublic' => 'nullable|boolean',
|
||||
|
|
|
|||
|
|
@ -92,7 +92,9 @@ protected function rules(): array
|
|||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'keydbConf' => 'nullable|string',
|
||||
'keydbPassword' => ValidationPatterns::databasePasswordRules(),
|
||||
'keydbPassword' => ValidationPatterns::databasePasswordRules(
|
||||
enforcePattern: $this->keydbPassword !== $this->database->keydb_password,
|
||||
),
|
||||
'image' => 'required|string',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'isPublic' => 'nullable|boolean',
|
||||
|
|
|
|||
|
|
@ -74,10 +74,18 @@ protected function rules(): array
|
|||
return [
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'mariadbRootPassword' => ValidationPatterns::databasePasswordRules(),
|
||||
'mariadbUser' => ValidationPatterns::databaseIdentifierRules(),
|
||||
'mariadbPassword' => ValidationPatterns::databasePasswordRules(),
|
||||
'mariadbDatabase' => ValidationPatterns::databaseIdentifierRules(),
|
||||
'mariadbRootPassword' => ValidationPatterns::databasePasswordRules(
|
||||
enforcePattern: $this->mariadbRootPassword !== $this->database->mariadb_root_password,
|
||||
),
|
||||
'mariadbUser' => ValidationPatterns::databaseIdentifierRules(
|
||||
enforcePattern: $this->mariadbUser !== $this->database->mariadb_user,
|
||||
),
|
||||
'mariadbPassword' => ValidationPatterns::databasePasswordRules(
|
||||
enforcePattern: $this->mariadbPassword !== $this->database->mariadb_password,
|
||||
),
|
||||
'mariadbDatabase' => ValidationPatterns::databaseIdentifierRules(
|
||||
enforcePattern: $this->mariadbDatabase !== $this->database->mariadb_database,
|
||||
),
|
||||
'mariadbConf' => 'nullable',
|
||||
'image' => 'required',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
|
|
|
|||
|
|
@ -75,9 +75,15 @@ protected function rules(): array
|
|||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'mongoConf' => 'nullable',
|
||||
'mongoInitdbRootUsername' => ValidationPatterns::databaseIdentifierRules(),
|
||||
'mongoInitdbRootPassword' => ValidationPatterns::databasePasswordRules(),
|
||||
'mongoInitdbDatabase' => ValidationPatterns::databaseIdentifierRules(),
|
||||
'mongoInitdbRootUsername' => ValidationPatterns::databaseIdentifierRules(
|
||||
enforcePattern: $this->mongoInitdbRootUsername !== $this->database->mongo_initdb_root_username,
|
||||
),
|
||||
'mongoInitdbRootPassword' => ValidationPatterns::databasePasswordRules(
|
||||
enforcePattern: $this->mongoInitdbRootPassword !== $this->database->mongo_initdb_root_password,
|
||||
),
|
||||
'mongoInitdbDatabase' => ValidationPatterns::databaseIdentifierRules(
|
||||
enforcePattern: $this->mongoInitdbDatabase !== $this->database->mongo_initdb_database,
|
||||
),
|
||||
'image' => 'required',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
'isPublic' => 'nullable|boolean',
|
||||
|
|
|
|||
|
|
@ -76,10 +76,18 @@ protected function rules(): array
|
|||
return [
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'mysqlRootPassword' => ValidationPatterns::databasePasswordRules(),
|
||||
'mysqlUser' => ValidationPatterns::databaseIdentifierRules(),
|
||||
'mysqlPassword' => ValidationPatterns::databasePasswordRules(),
|
||||
'mysqlDatabase' => ValidationPatterns::databaseIdentifierRules(),
|
||||
'mysqlRootPassword' => ValidationPatterns::databasePasswordRules(
|
||||
enforcePattern: $this->mysqlRootPassword !== $this->database->mysql_root_password,
|
||||
),
|
||||
'mysqlUser' => ValidationPatterns::databaseIdentifierRules(
|
||||
enforcePattern: $this->mysqlUser !== $this->database->mysql_user,
|
||||
),
|
||||
'mysqlPassword' => ValidationPatterns::databasePasswordRules(
|
||||
enforcePattern: $this->mysqlPassword !== $this->database->mysql_password,
|
||||
),
|
||||
'mysqlDatabase' => ValidationPatterns::databaseIdentifierRules(
|
||||
enforcePattern: $this->mysqlDatabase !== $this->database->mysql_database,
|
||||
),
|
||||
'mysqlConf' => 'nullable',
|
||||
'image' => 'required',
|
||||
'portsMappings' => ValidationPatterns::portMappingRules(),
|
||||
|
|
|
|||
|
|
@ -86,9 +86,15 @@ protected function rules(): array
|
|||
return [
|
||||
'name' => ValidationPatterns::nameRules(),
|
||||
'description' => ValidationPatterns::descriptionRules(),
|
||||
'postgresUser' => ValidationPatterns::databaseIdentifierRules(),
|
||||
'postgresPassword' => ValidationPatterns::databasePasswordRules(),
|
||||
'postgresDb' => ValidationPatterns::databaseIdentifierRules(),
|
||||
'postgresUser' => ValidationPatterns::databaseIdentifierRules(
|
||||
enforcePattern: $this->postgresUser !== $this->database->postgres_user,
|
||||
),
|
||||
'postgresPassword' => ValidationPatterns::databasePasswordRules(
|
||||
enforcePattern: $this->postgresPassword !== $this->database->postgres_password,
|
||||
),
|
||||
'postgresDb' => ValidationPatterns::databaseIdentifierRules(
|
||||
enforcePattern: $this->postgresDb !== $this->database->postgres_db,
|
||||
),
|
||||
'postgresInitdbArgs' => 'nullable',
|
||||
'postgresHostAuthMethod' => 'nullable',
|
||||
'postgresConf' => 'nullable',
|
||||
|
|
|
|||
|
|
@ -81,8 +81,12 @@ protected function rules(): array
|
|||
'publicPortTimeout' => 'nullable|integer|min:1',
|
||||
'isLogDrainEnabled' => 'nullable|boolean',
|
||||
'customDockerRunOptions' => 'nullable',
|
||||
'redisUsername' => ValidationPatterns::databaseIdentifierRules(),
|
||||
'redisPassword' => ValidationPatterns::databasePasswordRules(),
|
||||
'redisUsername' => ValidationPatterns::databaseIdentifierRules(
|
||||
enforcePattern: $this->redisUsername !== $this->database->redis_username,
|
||||
),
|
||||
'redisPassword' => ValidationPatterns::databasePasswordRules(
|
||||
enforcePattern: $this->redisPassword !== $this->database->redis_password,
|
||||
),
|
||||
'enableSsl' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,8 +82,12 @@ class ValidationPatterns
|
|||
|
||||
/**
|
||||
* Get validation rules for database identifier fields (username, database name).
|
||||
*
|
||||
* Set $enforcePattern to false to skip the regex check (for example when
|
||||
* re-validating a legacy value on an existing record that has not been
|
||||
* changed by the user). The length and type rules are always applied.
|
||||
*/
|
||||
public static function databaseIdentifierRules(bool $required = true, int $minLength = 1, int $maxLength = 63): array
|
||||
public static function databaseIdentifierRules(bool $required = true, int $minLength = 1, int $maxLength = 63, bool $enforcePattern = true): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
|
|
@ -96,7 +100,10 @@ public static function databaseIdentifierRules(bool $required = true, int $minLe
|
|||
$rules[] = 'string';
|
||||
$rules[] = "min:$minLength";
|
||||
$rules[] = "max:$maxLength";
|
||||
$rules[] = 'regex:'.self::DB_IDENTIFIER_PATTERN;
|
||||
|
||||
if ($enforcePattern) {
|
||||
$rules[] = 'regex:'.self::DB_IDENTIFIER_PATTERN;
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
|
@ -117,8 +124,12 @@ public static function databaseIdentifierMessages(string $field, string $label =
|
|||
|
||||
/**
|
||||
* Get validation rules for database password fields.
|
||||
*
|
||||
* Set $enforcePattern to false to skip the regex check (for example when
|
||||
* re-validating a legacy value on an existing record that has not been
|
||||
* changed by the user). The length and type rules are always applied.
|
||||
*/
|
||||
public static function databasePasswordRules(bool $required = true, int $minLength = 1, int $maxLength = 128): array
|
||||
public static function databasePasswordRules(bool $required = true, int $minLength = 1, int $maxLength = 128, bool $enforcePattern = true): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
|
|
@ -131,7 +142,10 @@ public static function databasePasswordRules(bool $required = true, int $minLeng
|
|||
$rules[] = 'string';
|
||||
$rules[] = "min:$minLength";
|
||||
$rules[] = "max:$maxLength";
|
||||
$rules[] = 'regex:'.self::DB_PASSWORD_PATTERN;
|
||||
|
||||
if ($enforcePattern) {
|
||||
$rules[] = 'regex:'.self::DB_PASSWORD_PATTERN;
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
|
|
|||
87
tests/Unit/DatabaseCredentialDirtyValidationTest.php
Normal file
87
tests/Unit/DatabaseCredentialDirtyValidationTest.php
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
use App\Support\ValidationPatterns;
|
||||
|
||||
// ── databasePasswordRules ─────────────────────────────────────────────────────
|
||||
|
||||
it('databasePasswordRules includes regex rule by default', function () {
|
||||
$rules = ValidationPatterns::databasePasswordRules();
|
||||
|
||||
$regexRules = array_filter($rules, fn ($rule) => str_starts_with($rule, 'regex:'));
|
||||
expect($regexRules)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('databasePasswordRules includes regex rule when enforcePattern true', function () {
|
||||
$rules = ValidationPatterns::databasePasswordRules(enforcePattern: true);
|
||||
|
||||
$regexRules = array_filter($rules, fn ($rule) => str_starts_with($rule, 'regex:'));
|
||||
expect($regexRules)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('databasePasswordRules omits regex rule when enforcePattern false', function () {
|
||||
$rules = ValidationPatterns::databasePasswordRules(enforcePattern: false);
|
||||
|
||||
$regexRules = array_filter($rules, fn ($rule) => str_starts_with($rule, 'regex:'));
|
||||
expect($regexRules)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('databasePasswordRules keeps required, string, min and max when enforcePattern false', function () {
|
||||
$rules = ValidationPatterns::databasePasswordRules(required: true, minLength: 1, maxLength: 128, enforcePattern: false);
|
||||
|
||||
expect($rules)->toContain('required');
|
||||
expect($rules)->toContain('string');
|
||||
expect($rules)->toContain('min:1');
|
||||
expect($rules)->toContain('max:128');
|
||||
});
|
||||
|
||||
it('databasePasswordRules keeps nullable and bounds when not required and enforcePattern false', function () {
|
||||
$rules = ValidationPatterns::databasePasswordRules(required: false, minLength: 2, maxLength: 64, enforcePattern: false);
|
||||
|
||||
expect($rules)->toContain('nullable');
|
||||
expect($rules)->toContain('string');
|
||||
expect($rules)->toContain('min:2');
|
||||
expect($rules)->toContain('max:64');
|
||||
expect(array_filter($rules, fn ($rule) => str_starts_with($rule, 'regex:')))->toBeEmpty();
|
||||
});
|
||||
|
||||
// ── databaseIdentifierRules ───────────────────────────────────────────────────
|
||||
|
||||
it('databaseIdentifierRules includes regex rule by default', function () {
|
||||
$rules = ValidationPatterns::databaseIdentifierRules();
|
||||
|
||||
$regexRules = array_filter($rules, fn ($rule) => str_starts_with($rule, 'regex:'));
|
||||
expect($regexRules)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('databaseIdentifierRules includes regex rule when enforcePattern true', function () {
|
||||
$rules = ValidationPatterns::databaseIdentifierRules(enforcePattern: true);
|
||||
|
||||
$regexRules = array_filter($rules, fn ($rule) => str_starts_with($rule, 'regex:'));
|
||||
expect($regexRules)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('databaseIdentifierRules omits regex rule when enforcePattern false', function () {
|
||||
$rules = ValidationPatterns::databaseIdentifierRules(enforcePattern: false);
|
||||
|
||||
$regexRules = array_filter($rules, fn ($rule) => str_starts_with($rule, 'regex:'));
|
||||
expect($regexRules)->toBeEmpty();
|
||||
});
|
||||
|
||||
it('databaseIdentifierRules keeps required, string, min and max when enforcePattern false', function () {
|
||||
$rules = ValidationPatterns::databaseIdentifierRules(required: true, minLength: 1, maxLength: 63, enforcePattern: false);
|
||||
|
||||
expect($rules)->toContain('required');
|
||||
expect($rules)->toContain('string');
|
||||
expect($rules)->toContain('min:1');
|
||||
expect($rules)->toContain('max:63');
|
||||
});
|
||||
|
||||
it('databaseIdentifierRules keeps nullable and bounds when not required and enforcePattern false', function () {
|
||||
$rules = ValidationPatterns::databaseIdentifierRules(required: false, minLength: 1, maxLength: 30, enforcePattern: false);
|
||||
|
||||
expect($rules)->toContain('nullable');
|
||||
expect($rules)->toContain('string');
|
||||
expect($rules)->toContain('min:1');
|
||||
expect($rules)->toContain('max:30');
|
||||
expect(array_filter($rules, fn ($rule) => str_starts_with($rule, 'regex:')))->toBeEmpty();
|
||||
});
|
||||
Loading…
Reference in a new issue