Fix FLEXIAPI-281 Restrict external_domains unicity on username, domain

This commit is contained in:
Timothée Jaussoin 2025-03-26 10:25:42 +00:00
parent deeea0ddb6
commit b493e9006e
10 changed files with 53 additions and 18 deletions

View file

@ -19,11 +19,12 @@
namespace App\Http\Controllers\Admin;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Space;
use App\Rules\Ini;
use App\Rules\Domain;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
class SpaceController extends Controller
@ -63,9 +64,9 @@ class SpaceController extends Controller
$request->merge(['full_host' => $fullHost]);
$request->validate([
'name' => 'required|unique:spaces',
'domain' => 'required|unique:spaces|regex:/'. Space::DOMAIN_REGEX . '/',
'domain' => ['required', 'unique:spaces', new Domain()],
'host' => 'nullable|regex:/'. Space::HOST_REGEX . '/',
'full_host' => 'required|unique:spaces,host',
'full_host' => ['required', 'unique:spaces,host', new Domain()],
]);
$space = new Space();

View file

@ -20,6 +20,7 @@
namespace App\Http\Controllers\Api\Admin;
use App\Space;
use App\Rules\Domain;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
@ -36,8 +37,8 @@ class SpaceController extends Controller
{
$request->validate([
'name' => 'required|unique:spaces',
'domain' => 'required|unique:spaces',
'host' => 'required|unique:spaces',
'domain' => ['required', 'unique:spaces', new Domain()],
'host' => ['required', 'unique:spaces', new Domain()],
'max_accounts' => 'nullable|integer',
'expire_at' => 'nullable|date|after_or_equal:today'
]);

View file

@ -23,14 +23,24 @@ use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\ExternalAccount;
use App\Rules\SIPUsername;
use App\Rules\Domain;
class CreateUpdate extends FormRequest
{
public function rules()
{
$usernameValidation = Rule::unique('external_accounts')->where(function ($query) {
return $query->where('username', $this->username)->where('domain', $this->domain);
});
if ($this->method() == 'POST') {
$usernameValidation = $usernameValidation->ignore($this->route('account'), 'account_id');
}
return [
'username' => 'required',
'domain' => 'required',
'username' => ['required', $usernameValidation, new SIPUsername()],
'domain' => ['required', new Domain()],
'realm' => 'different:domain',
'registrar' => 'different:domain',
'outbound_proxy' => 'different:domain',

View file

@ -0,0 +1,21 @@
<?php
namespace App\Rules;
use App\Space;
use Illuminate\Contracts\Validation\Rule;
use Respect\Validation\Validator;
class Domain implements Rule
{
public function passes($attribute, $value)
{
return Validator::regex('/' . Space::DOMAIN_REGEX . '/')->validate($value);
}
public function message()
{
return 'The :attribute should be a valid domain';
}
}

View file

@ -28,7 +28,7 @@ class Space extends Model
];
public const HOST_REGEX = '[\w\-]+';
public const DOMAIN_REGEX = '[\w\-\.]+';
public const DOMAIN_REGEX = '(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)';
public function accounts()
{

View file

@ -23,6 +23,8 @@ return new class extends Migration
$table->foreign('account_id')->references('id')
->on('accounts')->onDelete('cascade');
$table->unique(['username', 'domain']);
$table->timestamps();
});
}

View file

@ -82,7 +82,7 @@
@include('parts.errors', ['name' => __('email')])
@if (!empty($account->email))
<p class="oppose">
<p>
<a href="{{ route('admin.account.reset_password_email.create', $account) }}">
{{ __('Send an email to the user to reset the password') }}
</a>

View file

@ -34,6 +34,7 @@
<input placeholder="domain.tld" required="required" name="domain" type="text"
value="@if($externalAccount->id){{ $externalAccount->domain }}@else{{ old('domain') }}@endif">
<label for="domain">{{ __('Domain') }}</label>
@include('parts.errors', ['name' => 'domain'])
</div>
<div>

View file

@ -41,7 +41,6 @@
<li>{{ __('Realm') }} (different than domain)</li>
<li>{{ __('Registrar') }} (different than domain)</li>
<li>{{ __('Outbound Proxy') }} (different than domain)</li>
<li>{{ __('Encrypted') }} (yes or no)</li>
<li>{{ __('Protocol') }} (UDP, TCP or TLS)</li>
</ol>

View file

@ -42,16 +42,16 @@ class ApiAccountExternalAccountTest extends TestCase
$this->keyAuthenticated($admin)
->json($this->method, $this->route . '/' . $account->id . '/external/', [
'username' => $username,
'domain' => 'bar',
'domain' => 'bar.dev',
'password' => 'password',
'protocol' => 'UDP'
])->assertStatus(201);
$this->keyAuthenticated($admin)
->json($this->method, $this->route . '/' . $account->id . '/external/', [
'username' => $username,
'domain' => 'bar',
'registrar' => 'bar',
'username' => $username . '_two',
'domain' => 'bar.dev',
'registrar' => 'bar.dev',
'password' => 'password',
'protocol' => 'UDP'
])->assertJsonValidationErrors(['registrar']);
@ -70,14 +70,14 @@ class ApiAccountExternalAccountTest extends TestCase
$this->keyAuthenticated($admin)
->json($this->method, $this->route . '/' . $account->id . '/external/', [
'username' => $username . '2',
'domain' => 'bar',
'domain' => 'bar.dev',
'protocol' => 'UDP'
])->assertStatus(200);
$this->keyAuthenticated($admin)
->json($this->method, $this->route . '/' . $account->id . '/external/', [
'username' => $username . '2',
'domain' => 'bar',
'username' => $username . '3',
'domain' => 'bar.dev',
'realm' => 'newrealm',
'protocol' => 'UDP'
])->assertJsonValidationErrors(['password']);