Fix FLEXIAPI-181 Replace APP_ADMINS_MANAGE_MULTI_DOMAINS with APP_SUPER_ADMINS_SIP_DOMAINS

This commit is contained in:
Timothée Jaussoin 2024-06-10 13:49:42 +00:00
parent 0f3454fb68
commit 0d399503c4
22 changed files with 94 additions and 117 deletions

View file

@ -2,6 +2,7 @@
v1.5
----
- Fix FLEXIAPI-181 Replace APP_ADMINS_MANAGE_MULTI_DOMAINS with APP_SUPER_ADMINS_SIP_DOMAINS
- Fix FLEXIAPI-180 Fix the token and activation flow for the provisioning with token endpoint when the header is missing
- Fix FLEXIAPI-179 Add Localization support as a Middleware that handles Accept-Language HTTP header
- Fix FLEXIAPI-178 Show the unused code in the Activity tab of the accounts in the admin panel

View file

@ -4,6 +4,7 @@ APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
APP_SIP_DOMAIN=sip.example.com
APP_SUPER_ADMINS_SIP_DOMAINS= # A comma separated list of sip domains that has their admins super admins
APP_LINPHONE_DAEMON_UNIX_PATH=
APP_FLEXISIP_PUSHER_PATH=
@ -15,7 +16,6 @@ APP_API_ACCOUNT_CREATION_TOKEN_RETRY_MINUTES=60 # Number of minutes between two
APP_ALLOW_PHONE_NUMBER_USERNAME_ADMIN_API=false # Allow phone numbers to be set as username in admin account creation endpoints
# Risky toggles
APP_ADMINS_MANAGE_MULTI_DOMAINS=false # Allow admins to handle all the accounts in the database
APP_DANGEROUS_ENDPOINTS=false # Enable some dangerous endpoints used for XMLRPC like fallback usage
# SIP server parameters

View file

@ -68,7 +68,7 @@ class Account extends Authenticatable
protected static function booted()
{
static::addGlobalScope('domain', function (Builder $builder) {
if (Auth::hasUser() && Auth::user()->admin && config('app.admins_manage_multi_domains')) {
if (Auth::hasUser() && Auth::user()->superAdmin) {
return;
}
@ -321,6 +321,23 @@ class Account extends Authenticatable
return self::$dtmfProtocols[$this->attributes['dtmf_protocol']];
}
public function getSuperAdminAttribute(): bool
{
$domains = config('app.super_admins_sip_domains');
if (empty($domains)) {
return false;
}
$domains = explode(',', $domains);
if (empty($domains)) {
return false;
}
return $this->admin && in_array($this->domain, $domains);
}
/**
* Utils
*/

View file

@ -111,8 +111,7 @@ function resolveDomain(Request $request): string
{
return $request->has('domain')
&& $request->user()
&& $request->user()->admin
&& config('app.admins_manage_multi_domains')
&& $request->user()->superAdmin
? $request->get('domain')
: config('app.sip_domain');
}

View file

@ -40,7 +40,7 @@ class AccountController extends Controller
return view('account.blocked');
}
public function panel(Request $request)
public function dashboard(Request $request)
{
return view('account.dashboard', [
'account' => $request->user()

View file

@ -33,7 +33,7 @@ class AuthenticateController extends Controller
public function login(Request $request)
{
if (auth()->user()) {
if (Auth::user()) {
return redirect()->route('account.dashboard');
}

View file

@ -27,6 +27,7 @@ use App\Http\Controllers\Controller;
use App\Libraries\StatisticsGraphFactory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class StatisticsController extends Controller
{
@ -65,7 +66,7 @@ class StatisticsController extends Controller
return $graph->export();
}
if (config('app.admins_manage_multi_domains')) {
if (Auth::user()?->superAdmin) {
switch ($type) {
case 'messages':
$domains = StatisticsMessage::groupBy('from_domain')->pluck('from_domain');

View file

@ -26,6 +26,7 @@ use Carbon\Carbon;
use Carbon\CarbonInterval;
use Carbon\CarbonPeriod;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
@ -56,7 +57,7 @@ class StatisticsGraphFactory
$fromQuery = StatisticsMessage::query();
$toQuery = StatisticsMessage::query();
if (!config('app.admins_manage_multi_domains')) {
if (!Auth::user()?->isAdmin) {
$fromQuery->where('from_domain', config('app.sip_domain'));
$toQuery->toDomain($this->domain);
} elseif ($this->domain) {
@ -88,7 +89,7 @@ class StatisticsGraphFactory
$fromQuery = StatisticsCall::query();
$toQuery = StatisticsCall::query();
if (!config('app.admins_manage_multi_domains')) {
if (!Auth::user()?->superAdmin) {
$fromQuery->where('from_domain', config('app.sip_domain'));
$toQuery->where('to_domain', config('app.sip_domain'));
} elseif ($this->domain) {
@ -125,7 +126,7 @@ class StatisticsGraphFactory
// Accounts doesn't have a from and to
$this->domain = $this->domain ?? $this->fromDomain;
if (!config('app.admins_manage_multi_domains')) {
if (!Auth::user()?->isAdmin) {
$this->data->where('domain', config('app.sip_domain'));
} elseif ($this->domain) {
$this->data->where('domain', $this->domain);

40
flexiapi/composer.lock generated
View file

@ -2921,16 +2921,16 @@
},
{
"name": "nesbot/carbon",
"version": "2.72.3",
"version": "2.72.5",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83"
"reference": "afd46589c216118ecd48ff2b95d77596af1e57ed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/0c6fd108360c562f6e4fd1dedb8233b423e91c83",
"reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/afd46589c216118ecd48ff2b95d77596af1e57ed",
"reference": "afd46589c216118ecd48ff2b95d77596af1e57ed",
"shasum": ""
},
"require": {
@ -2964,8 +2964,8 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-3.x": "3.x-dev",
"dev-master": "2.x-dev"
"dev-master": "3.x-dev",
"dev-2.x": "2.x-dev"
},
"laravel": {
"providers": [
@ -3024,7 +3024,7 @@
"type": "tidelift"
}
],
"time": "2024-01-25T10:35:09+00:00"
"time": "2024-06-03T19:18:41+00:00"
},
{
"name": "nette/schema",
@ -4508,16 +4508,16 @@
},
{
"name": "psy/psysh",
"version": "v0.12.3",
"version": "v0.12.4",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
"reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73"
"reference": "2fd717afa05341b4f8152547f142cd2f130f6818"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73",
"reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/2fd717afa05341b4f8152547f142cd2f130f6818",
"reference": "2fd717afa05341b4f8152547f142cd2f130f6818",
"shasum": ""
},
"require": {
@ -4581,9 +4581,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.12.3"
"source": "https://github.com/bobthecow/psysh/tree/v0.12.4"
},
"time": "2024-04-02T15:57:53+00:00"
"time": "2024-06-10T01:18:23+00:00"
},
{
"name": "ralouphie/getallheaders",
@ -5618,20 +5618,20 @@
},
{
"name": "scyllaly/hcaptcha",
"version": "4.4.5",
"version": "4.4.6",
"source": {
"type": "git",
"url": "https://github.com/Scyllaly/hcaptcha.git",
"reference": "3c133dfe684d34570e911de11098ebaa0d2c369d"
"reference": "5b7d5ec5430014bbf2b44831cef3ddf9d3ded451"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Scyllaly/hcaptcha/zipball/3c133dfe684d34570e911de11098ebaa0d2c369d",
"reference": "3c133dfe684d34570e911de11098ebaa0d2c369d",
"url": "https://api.github.com/repos/Scyllaly/hcaptcha/zipball/5b7d5ec5430014bbf2b44831cef3ddf9d3ded451",
"reference": "5b7d5ec5430014bbf2b44831cef3ddf9d3ded451",
"shasum": ""
},
"require": {
"illuminate/support": "5.*|6.*|7.*|8.*|^9.0|10.*",
"illuminate/support": "5.*|6.*|7.*|8.*|^9.0|10.*|^11.0",
"php": ">=5.5.5"
},
"require-dev": {
@ -5671,9 +5671,9 @@
],
"support": {
"issues": "https://github.com/Scyllaly/hcaptcha/issues",
"source": "https://github.com/Scyllaly/hcaptcha/tree/4.4.5"
"source": "https://github.com/Scyllaly/hcaptcha/tree/4.4.6"
},
"time": "2023-03-14T16:36:21+00:00"
"time": "2024-06-08T15:55:53+00:00"
},
{
"name": "sebastian/cli-parser",

View file

@ -15,6 +15,7 @@ return [
'name' => env('APP_NAME', 'Account Manager'),
'sip_domain' => env('APP_SIP_DOMAIN', 'sip.domain.com'),
'super_admins_sip_domains' => env('APP_SUPER_ADMINS_SIP_DOMAINS', ''),
'project_url' => env('APP_PROJECT_URL', ''),
'terms_of_use_url' => env('TERMS_OF_USE_URL', ''),
@ -72,12 +73,6 @@ return [
*/
'realm' => env('ACCOUNT_REALM', null),
/**
* Allow admins to handle all the accounts in the database
* ENABLE IT AT YOUR OWN RISKS IN PRODUCTION
*/
'admins_manage_multi_domains' => env('APP_ADMINS_MANAGE_MULTI_DOMAINS', false),
/**
* /!\ Enable dangerous endpoints required for fallback
*/

View file

@ -330,6 +330,10 @@ header nav a.oppose~a.oppose {
margin-left: 0;
}
header nav span.badge {
margin-left: 1rem;
}
@media screen and (max-width: 800px) {
header nav {
padding: 1rem;

View file

@ -45,7 +45,7 @@
@include('parts.errors', ['name' => 'username'])
</div>
<div>
<input placeholder="domain.com" @if (config('app.admins_manage_multi_domains')) required @else disabled @endif name="domain"
<input placeholder="domain.com" @if (auth()->user()?->superAdmin) required @else disabled @endif name="domain"
type="text" value="{{ $account->domain ?? config('app.sip_domain') }}"
@if ($account->id) readonly @endif>
<label for="domain">Domain</label>

View file

@ -35,19 +35,7 @@
<label for="search">Search</label>
</div>
<div class="large on_desktop"></div>
<div class="select">
<select name="domain" onchange="this.form.submit()">
<option value="">
Select a domain
</option>
@foreach ($domains as $d)
<option value="{{ $d }}" @if (request()->get('domain', '') == $d) selected="selected" @endif>
{{ $d }}
</option>
@endforeach
</select>
<label for="domain">Domain</label>
</div>
@include('admin.account.parts.forms.select_domain')
<div class="select">
<select name="contacts_list" onchange="this.form.submit()">
<option value="">
@ -107,7 +95,9 @@
@if ($account->activated)
<span class="badge badge-success" title="Activated">Act.</span>
@endif
@if ($account->admin)
@if ($account->superAdmin)
<span class="badge badge-error" title="Admin">Super Adm.</span>
@elseif ($account->admin)
<span class="badge badge-primary" title="Admin">Adm.</span>
@endif
@if ($account->sha256Password)

View file

@ -0,0 +1,16 @@
@if (auth()->user() && auth()->user()->superAdmin && count($domains) > 0)
<div class="select">
<select name="domain" onchange="this.form.submit()">
<option value="">
Select a domain
</option>
@foreach ($domains as $d)
<option value="{{ $d }}"
@if (request()->get('domain', '') == $d) selected="selected" @endif>
{{ $d }}
</option>
@endforeach
</select>
<label for="domain">Domain</label>
</div>
@endif

View file

@ -35,19 +35,7 @@
value="{{ request()->get('search', '') }}">
<label for="search">Search</label>
</div>
<div class="select">
<select name="domain" onchange="this.form.submit()">
<option value="">
Select a domain
</option>
@foreach ($domains as $d)
<option value="{{ $d }}" @if (request()->get('domain', '') == $d) selected="selected" @endif>
{{ $d }}
</option>
@endforeach
</select>
<label for="domain">Domain</label>
</div>
@include('admin.account.parts.forms.select_domain')
<div>
<a href="{{ route('admin.contacts_lists.contacts.add', $contacts_list->id) }}" type="reset"
class="btn btn-secondary">Reset</a>

View file

@ -73,19 +73,7 @@
value="{{ request()->get('search', '') }}">
<label for="search">Search</label>
</div>
<div class="select">
<select name="domain" onchange="this.form.submit()">
<option value="">
Select a domain
</option>
@foreach ($domains as $d)
<option value="{{ $d }}" @if (request()->get('domain', '') == $d) selected="selected" @endif>
{{ $d }}
</option>
@endforeach
</select>
<label for="domain">Domain</label>
</div>
@include('admin.account.parts.forms.select_domain')
<div>
<a href="{{ route('admin.contacts_lists.edit', $contacts_list->id) }}" type="reset"
class="btn btn-secondary">Reset</a>

View file

@ -26,22 +26,7 @@
class="chip @if ($request->get('by', 'day') == 'year') selected @endif">Year</a>
</div>
@if (config('app.admins_manage_multi_domains'))
<div class="select">
<select name="domain" onchange="this.form.submit()">
<option value="">
Select a domain
</option>
@foreach ($domains as $d)
<option value="{{ $d }}"
@if (request()->get('domain', '') == $d) selected="selected" @endif>
{{ $d }}
</option>
@endforeach
</select>
<label for="domain">Domain</label>
</div>
@endif
@include('admin.account.parts.forms.select_domain')
<div class="select">
<select name="contacts_list" onchange="this.form.submit()">

View file

@ -27,22 +27,7 @@
<div class="large on_desktop"></div>
@if (config('app.admins_manage_multi_domains'))
<div class="select">
<select name="domain" onchange="this.form.submit()">
<option value="">
Select a domain
</option>
@foreach ($domains as $d)
<option value="{{ $d }}"
@if (request()->get('domain', '') == $d) selected="selected" @endif>
{{ $d }}
</option>
@endforeach
</select>
<label for="domain">Domain</label>
</div>
@endif
@include('admin.account.parts.forms.select_domain')
<div class="select">
<select name="contacts_list" onchange="this.form.submit()">

View file

@ -336,7 +336,7 @@ JSON parameters:
* `username` unique username, minimum 6 characters
* `password` required minimum 6 characters
* `algorithm` required, values can be `SHA-256` or `MD5`
* `domain` **not configurable by default**. Only configurable if `APP_ADMINS_MANAGE_MULTI_DOMAINS` is set to `true` in the global configuration. Otherwise `APP_SIP_DOMAIN` is used.
* `domain` **not configurable by default**. Only configurable if the admin is a super admin. Otherwise `APP_SIP_DOMAIN` is used.
* `activated` optional, a boolean, set to `false` by default
* `display_name` optional, string
* `email` optional, must be an email, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
@ -354,7 +354,7 @@ Update an existing account. Ensure to resend all the parameters to not reset the
JSON parameters:
* `username` unique username, minimum 6 characters
* `domain` **not configurable by default**. Only configurable if `APP_ADMINS_MANAGE_MULTI_DOMAINS` is set to `true` in the global configuration. Otherwise `APP_SIP_DOMAIN` is used.
* `domain` **not configurable by default**. Only configurable if the admin is a super admin. Otherwise `APP_SIP_DOMAIN` is used.
* `password` required minimum 6 characters
* `algorithm` required, values can be `SHA-256` or `MD5`
* `display_name` optional, string

View file

@ -34,8 +34,14 @@
</a>
@if (auth()->user())
<a class="oppose" href="{{ route('account.dashboard') }}">
<i class="material-symbols-outlined">account_circle</i><span
class="on_desktop">{{ auth()->user()->identifier }}</span>
<i class="material-symbols-outlined">account_circle</i>
<span class="on_desktop">{{ auth()->user()->identifier }}</span>
@if (auth()->user()->superAdmin)
<span class="badge badge-error" title="Admin">Super Adm.</span>
@elseif (auth()->user()->admin)
<span class="badge badge-primary" title="Admin">Adm.</span>
@endif
</a>
<a class="oppose" href="{{ route('account.logout') }}">
<i class="material-symbols-outlined">logout</i>

View file

@ -128,7 +128,7 @@ Route::group(['middleware' => 'web_panel_enabled'], function () {
});
Route::controller(AccountController::class)->group(function () {
Route::get('dashboard', 'panel')->name('dashboard');
Route::get('dashboard', 'dashboard')->name('dashboard');
Route::get('delete', 'delete')->name('delete');
Route::delete('delete', 'destroy')->name('destroy');

View file

@ -180,7 +180,7 @@ class ApiAccountTest extends TestCase
{
$configDomain = 'sip.domain.com';
config()->set('app.sip_domain', $configDomain);
config()->set('app.admins_manage_multi_domains', true);
config()->set('app.super_admins_sip_domains', $configDomain);
$password = Password::factory()->admin()->create();
$password->account->generateApiKey();
@ -242,8 +242,9 @@ class ApiAccountTest extends TestCase
public function testDomainInTestDeployment()
{
$configDomain = 'testdomain.com';
config()->set('app.admins_manage_multi_domains', true);
config()->set('app.sip_domain', 'anotherdomain.com');
$adminDomain = 'admindomain.com';
config()->set('app.super_admins_sip_domains', $adminDomain);
config()->set('app.sip_domain', $adminDomain);
$password = Password::factory()->admin()->create();
$username = 'foobar';