Fix FLEXIAPI-400 Scope the API accounts endpoints per space

This commit is contained in:
Timothée Jaussoin 2025-11-27 16:58:58 +01:00
parent 6fcde1b467
commit 593d7ce5c0
16 changed files with 210 additions and 143 deletions

View file

@ -23,7 +23,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Collection;
use Carbon\Carbon;

View file

@ -10,12 +10,12 @@ class VcardsStorageController extends Controller
{
public function index(Request $request)
{
return (new AdminVcardsStorageController)->index($request->user()->id);
return (new AdminVcardsStorageController)->index($request, $request->user()->id);
}
public function show(Request $request, string $uuid)
{
return (new AdminVcardsStorageController)->show($request->user()->id, $uuid);
return (new AdminVcardsStorageController)->show($request, $request->user()->id, $uuid);
}
public function store(Request $request)
@ -30,6 +30,6 @@ class VcardsStorageController extends Controller
public function destroy(Request $request, string $uuid)
{
return (new AdminVcardsStorageController)->destroy($request->user()->id, $uuid);
return (new AdminVcardsStorageController)->destroy($request, $request->user()->id, $uuid);
}
}

View file

@ -22,28 +22,27 @@ namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Account;
use App\AccountAction;
use App\Rules\NoUppercase;
class ActionController extends Controller
{
public function index(int $id)
public function index(Request $request, int $accountId)
{
return $this->resolveAccount($id)->actions;
return $this->resolveAccount($request, $accountId)->actions;
}
public function get(int $id, int $actionId)
public function get(Request $request, int $accountId, int $actionId)
{
return $this->resolveAccount($id)
return $this->resolveAccount($request, $accountId)
->actions()
->where('id', $actionId)
->firstOrFail();
}
public function store(Request $request, int $id)
public function store(Request $request, int $accountId)
{
$account = $this->resolveAccount($id);
$account = $this->resolveAccount($request, $accountId);
$request->validate([
'key' => ['required', 'alpha_dash', new NoUppercase],
@ -59,9 +58,9 @@ class ActionController extends Controller
return $accountAction;
}
public function update(Request $request, int $id, int $actionId)
public function update(Request $request, int $accountId, int $actionId)
{
$account = $this->resolveAccount($id);
$account = $this->resolveAccount($request, $accountId);
$request->validate([
'key' => ['alpha_dash', new NoUppercase],
@ -79,17 +78,17 @@ class ActionController extends Controller
return $accountAction;
}
public function destroy(int $id, int $actionId)
public function destroy(Request $request, int $accountId, int $actionId)
{
return $this->resolveAccount($id)
return $this->resolveAccount($request, $accountId)
->actions()
->where('id', $actionId)
->delete();
}
private function resolveAccount(int $id)
private function resolveAccount(Request $request, int $accountId)
{
$account = Account::findOrFail($id);
$account = $request->space->accounts()->findOrFail($accountId);
if ($account->dtmf_protocol == null) abort(403, 'DTMF Protocol must be configured');
return $account;

View file

@ -20,37 +20,36 @@
namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller;
use App\Account;
use Illuminate\Http\Request;
class ContactController extends Controller
{
public function index(int $id)
public function index(Request $request, int $accountId)
{
return Account::findOrFail($id)->contacts;
return $request->space->accounts()->findOrFail($accountId)->contacts;
}
public function show(int $id, int $contactId)
public function show(Request $request, int $accountId, int $contactId)
{
return Account::findOrFail($id)
return $request->space->accounts()->findOrFail($accountId)
->contacts()
->where('id', $contactId)
->firstOrFail();
}
public function add(int $id, int $contactId)
public function add(Request $request, int $accountId, int $contactId)
{
$account = Account::findOrFail($id);
$account = $request->space->accounts()->findOrFail($accountId);
$account->contacts()->detach($contactId);
if (Account::findOrFail($contactId)) {
if ($request->space->accounts()->findOrFail($contactId)) {
return $account->contacts()->attach($contactId);
}
}
public function remove(int $id, int $contactId)
public function remove(Request $request, int $accountId, int $contactId)
{
$account = Account::findOrFail($id);
$account = $request->space->accounts()->findOrFail($accountId);
if (!$account->contacts()->pluck('id')->contains($contactId)) {
abort(404);

View file

@ -20,20 +20,20 @@
namespace App\Http\Controllers\Api\Admin\Account;
use App\Http\Controllers\Controller;
use App\Account;
use Illuminate\Http\Request;
class DictionaryController extends Controller
{
public function index(int $accountId)
public function index(Request $request, int $accountId)
{
return Account::findOrFail($accountId)->dictionary;
return $request->space->accounts()->findOrFail($accountId)->dictionary;
}
public function show(int $accountId, string $key)
public function show(Request $request, int $accountId, string $key)
{
return Account::findOrFail($accountId)->dictionaryEntries()->where('key', $key)->first();
return $request->space->accounts()
->findOrFail($accountId)->dictionaryEntries()->where('key', $key)->first();
}
public function set(Request $request, int $accountId, string $key)
@ -42,7 +42,7 @@ class DictionaryController extends Controller
'value' => 'required'
]);
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
$result = $account->setDictionaryEntry($key, $request->get('value'));
if (function_exists('accountServiceAccountEditedHook')) {
@ -53,8 +53,9 @@ class DictionaryController extends Controller
return $result;
}
public function destroy(int $accountId, string $key)
public function destroy(Request $request, int $accountId, string $key)
{
return Account::findOrFail($accountId)->dictionaryEntries()->where('key', $key)->delete();
return $request->space->accounts()
->findOrFail($accountId)->dictionaryEntries()->where('key', $key)->delete();
}
}

View file

@ -25,7 +25,6 @@ use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use App\Account;
use App\AccountTombstone;
use App\AccountType;
use App\ContactsList;
@ -35,18 +34,23 @@ use App\Http\Requests\Account\Update\Api\AsAdminRequest as ApiAsAdminRequest;
use App\Mail\Provisioning;
use App\Mail\ResetPassword;
use App\Services\AccountService;
use App\Space;
class AccountController extends Controller
{
public function index(Request $request)
{
return Account::without(['passwords', 'admin'])->with(['phoneChangeCode', 'emailChangeCode'])->paginate(20);
return $request->space->accounts()
->without(['passwords', 'admin'])
->with(['phoneChangeCode', 'emailChangeCode'])
->paginate(20);
}
public function show(Request $request, $accountId)
{
$account = Account::without(['passwords', 'admin'])->with(['phoneChangeCode', 'emailChangeCode'])->findOrFail($accountId);
$account = $request->space->accounts()
->without(['passwords', 'admin'])
->with(['phoneChangeCode', 'emailChangeCode'])
->findOrFail($accountId);
if ($request->user()->admin) {
if ($account->phoneChangeCode) {
@ -61,27 +65,29 @@ class AccountController extends Controller
return $account;
}
public function search(string $sip)
public function search(Request $request, string $sip)
{
$account = Account::sip($sip)->first();
$account = $request->space->accounts()->sip($sip)->first();
if (!$account) abort(404, 'SIP address not found');
if (!$account)
abort(404, 'SIP address not found');
return $account;
}
public function searchByEmail(string $email)
public function searchByEmail(Request $request, string $email)
{
$account = Account::where('email', $email)->first();
$account = $request->space->accounts()->where('email', $email)->first();
if (!$account) abort(404, 'Email address not found');
if (!$account)
abort(404, 'Email address not found');
return $account;
}
public function destroy(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
if (!$account->hasTombstone()) {
$tombstone = new AccountTombstone();
@ -95,9 +101,9 @@ class AccountController extends Controller
Log::channel('events')->info('API Admin: Account destroyed', ['id' => $account->identifier]);
}
public function activate(int $accountId)
public function activate(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
$account->activated = true;
$account->save();
@ -106,9 +112,9 @@ class AccountController extends Controller
return $account;
}
public function deactivate(int $accountId)
public function deactivate(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
$account->activated = false;
$account->save();
@ -117,9 +123,9 @@ class AccountController extends Controller
return $account;
}
public function block(int $accountId)
public function block(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
$account->blocked = true;
$account->save();
@ -128,9 +134,9 @@ class AccountController extends Controller
return $account;
}
public function unblock(int $accountId)
public function unblock(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
$account->blocked = false;
$account->save();
@ -139,9 +145,9 @@ class AccountController extends Controller
return $account;
}
public function provision(int $accountId)
public function provision(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
$account->provision();
$account->save();
@ -164,55 +170,56 @@ class AccountController extends Controller
return $account->makeVisible(['provisioning_token']);
}
public function typeAdd(int $accountId, int $typeId)
public function typeAdd(Request $request, int $accountId, int $typeId)
{
if (Account::findOrFail($accountId)->types()->pluck('id')->contains($typeId)) {
if ($request->space->accounts()->findOrFail($accountId)->types()->pluck('id')->contains($typeId)) {
abort(403);
}
if (AccountType::findOrFail($typeId)) {
return Account::findOrFail($accountId)->types()->attach($typeId);
return $request->space->accounts()->findOrFail($accountId)->types()->attach($typeId);
}
}
public function typeRemove(int $accountId, int $typeId)
public function typeRemove(Request $request, int $accountId, int $typeId)
{
if (!Account::findOrFail($accountId)->types()->pluck('id')->contains($typeId)) {
if (!$request->space->accounts()->findOrFail($accountId)->types()->pluck('id')->contains($typeId)) {
abort(403);
}
return Account::findOrFail($accountId)->types()->detach($typeId);
return $request->space->accounts()->findOrFail($accountId)->types()->detach($typeId);
}
public function contactsListAdd(int $accountId, int $contactsListId)
public function contactsListAdd(Request $request, int $accountId, int $contactsListId)
{
if (Account::findOrFail($accountId)->contactsLists()->pluck('id')->contains($contactsListId)) {
if ($request->space->accounts()->findOrFail($accountId)->contactsLists()->pluck('id')->contains($contactsListId)) {
abort(403);
}
if (ContactsList::findOrFail($contactsListId)) {
return Account::findOrFail($accountId)->contactsLists()->attach($contactsListId);
return $request->space->accounts()->findOrFail($accountId)->contactsLists()->attach($contactsListId);
}
}
public function contactsListRemove(int $accountId, int $contactsListId)
public function contactsListRemove(Request $request, int $accountId, int $contactsListId)
{
if (!Account::findOrFail($accountId)->contactsLists()->pluck('id')->contains($contactsListId)) {
if (!$request->space->accounts()->findOrFail($accountId)->contactsLists()->pluck('id')->contains($contactsListId)) {
abort(403);
}
return Account::findOrFail($accountId)->contactsLists()->detach($contactsListId);
return $request->space->accounts()->findOrFail($accountId)->contactsLists()->detach($contactsListId);
}
/**
* Emails
*/
public function sendProvisioningEmail(int $accountId)
public function sendProvisioningEmail(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
if (!$account->email) abort(403, 'No email configured');
if (!$account->email)
abort(403, 'No email configured');
$account->provision();
@ -221,11 +228,12 @@ class AccountController extends Controller
Log::channel('events')->info('API: Sending provisioning email', ['id' => $account->identifier]);
}
public function sendResetPasswordEmail(int $accountId)
public function sendResetPasswordEmail(Request $request, int $accountId)
{
$account = Account::findOrFail($accountId);
$account = $request->space->accounts()->findOrFail($accountId);
if (!$account->email) abort(403, 'No email configured');
if (!$account->email)
abort(403, 'No email configured');
$resetPasswordEmail = new ResetPasswordEmailToken;
$resetPasswordEmail->account_id = $account->id;

View file

@ -22,21 +22,27 @@ namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Libraries\FlexisipRedisConnector;
use App\Account;
use Illuminate\Http\Request;
use stdClass;
class DeviceController extends Controller
{
public function index(int $accountId)
public function index(Request $request, int $accountId)
{
$devices = (new FlexisipRedisConnector)->getDevices(Account::findOrFail($accountId)->identifier);
$devices = (new FlexisipRedisConnector)->getDevices(
$request->space->accounts()->findOrFail($accountId)->identifier
);
return ($devices->isEmpty()) ? new stdClass : $devices;
}
public function destroy(int $accountId, string $uuid)
public function destroy(Request $request, int $accountId, string $uuid)
{
$connector = new FlexisipRedisConnector;
return $connector->deleteDevice(Account::findOrFail($accountId)->identifier, $uuid);
return $connector->deleteDevice(
$request->space->accounts()->findOrFail($accountId)->identifier,
$uuid
);
}
}

View file

@ -23,16 +23,12 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\ExternalAccount\CreateUpdate;
use App\Services\AccountService;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use App\ExternalAccount;
use App\Account;
class ExternalAccountController extends Controller
{
public function show(int $accountId)
public function show(Request $request, int $accountId)
{
return Account::findOrFail($accountId)->external()->firstOrFail();
return $request->space->accounts()->findOrFail($accountId)->external()->firstOrFail();
}
public function store(CreateUpdate $request, int $accountId)

View file

@ -19,7 +19,6 @@
namespace App\Http\Controllers\Api\Admin\Space;
use App\Account;
use App\ContactsList;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
@ -79,7 +78,7 @@ class ContactsListController extends Controller
$contactsList = $request->space->contactsLists()->findOrFail($id);
$contactsList->contacts()->detach($contactId);
if (Account::findOrFail($contactId)) {
if ($request->space->accounts()->findOrFail($contactId)) {
return $contactsList->contacts()->attach($contactId);
}
}

View file

@ -19,7 +19,6 @@
namespace App\Http\Controllers\Api\Admin;
use App\Account;
use App\Http\Controllers\Controller;
use App\Rules\Vcard;
use App\VcardStorage;
@ -30,15 +29,15 @@ use stdClass;
class VcardsStorageController extends Controller
{
public function index(int $accountId)
public function index(Request $request, int $accountId)
{
$list = Account::findOrFail($accountId)->vcardsStorage()->get()->keyBy('uuid');
$list = $request->space->accounts()->findOrFail($accountId)->vcardsStorage()->get()->keyBy('uuid');
return $list->isEmpty() ? new stdClass : $list;
}
public function show(int $accountId, string $uuid)
public function show(Request $request, int $accountId, string $uuid)
{
return Account::findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
return $request->space->accounts()->findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
}
public function store(Request $request, int $accountId)
@ -49,7 +48,7 @@ class VcardsStorageController extends Controller
$vcardo = VObject\Reader::read($request->get('vcard'));
if (Account::findOrFail($accountId)->vcardsStorage()->where('uuid', $vcardo->UID)->first()) {
if ($request->space->accounts()->findOrFail($accountId)->vcardsStorage()->where('uuid', $vcardo->UID)->first()) {
abort(409, 'Vcard already exists');
}
@ -74,16 +73,16 @@ class VcardsStorageController extends Controller
abort(422, 'UUID should be the same');
}
$vcard = Account::findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
$vcard = $request->space->accounts()->findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
$vcard->vcard = preg_replace('/\r\n?/', "\n", $vcardo->serialize());
$vcard->save();
return $vcard;
}
public function destroy(int $accountId, string $uuid)
public function destroy(Request $request, int $accountId, string $uuid)
{
$vcard = Account::findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
$vcard = $request->space->accounts()->findOrFail($accountId)->vcardsStorage()->where('uuid', $uuid)->firstOrFail();
return $vcard->delete();
}

View file

@ -29,7 +29,9 @@ class AuthenticateAdmin
return redirect()->route('account.login');
}
if (!$request->user()->admin || $request->user()->domain != $request->space->domain) {
if (!$request->user()->admin
|| (!$request->user()->superAdmin && $request->user()->domain != $request->space->domain)
) {
return abort(403, 'Unauthorized area');
}

View file

@ -19,13 +19,11 @@
namespace Database\Factories;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;
use Awobaz\Compoships\Database\Eloquent\Factories\ComposhipsFactory;
use App\Account;
use App\AccountCreationToken;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Space;
class AccountFactory extends Factory

View file

@ -139,7 +139,7 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo
Route::post('/', 'store');
Route::put('{account_id}', 'update');
Route::get('/', 'index');
Route::get('/', 'index')->name('accounts.index');
Route::get('{account_id}', 'show');
Route::delete('{account_id}', 'destroy');
Route::get('{sip}/search', 'search');
@ -169,7 +169,7 @@ Route::group(['middleware' => ['auth.jwt', 'auth.digest_or_key', 'auth.check_blo
Route::apiResource('accounts/{id}/actions', ActionController::class);
Route::apiResource('account_types', TypeController::class);
Route::apiResource('accounts/{account_id}/vcards-storage', AdminVcardsStorageController::class);
Route::apiResource('accounts/{id}/vcards-storage', AdminVcardsStorageController::class);
Route::apiResource('contacts_lists', ContactsListController::class);
Route::prefix('contacts_lists')->controller(ContactsListController::class)->group(function () {

View file

@ -19,12 +19,9 @@
namespace Tests\Feature;
use App\Account;
use App\Space;
use App\AccountRecoveryToken;
use Tests\TestCase;
use Carbon\Carbon;
use App\Http\Middleware\IsWebPanelEnabled;
class ApiAccountRecoveryTokenTest extends TestCase
{

View file

@ -23,7 +23,6 @@ use App\Account;
use App\AccountTombstone;
use App\Password;
use App\Space;
use Carbon\Carbon;
use Tests\TestCase;
class ApiAccountTest extends TestCase
@ -185,60 +184,122 @@ class ApiAccountTest extends TestCase
public function testAdminMultiDomains()
{
$configDomain = 'sip2.example.com';
config()->set('app.sip_domain', $configDomain);
$account = Account::factory()->superAdmin()->create();
$account->generateUserApiKey();
$account->save();
$username = 'foobar';
$domain1 = Space::first()->domain;
$domain2 = Space::factory()->secondDomain()->create()->domain;
$space1 = Space::factory()->create();
$space2 = Space::factory()->secondDomain()->create();
$this->keyAuthenticated($account)
$superAdmin = Account::factory()->fromSpace($space1)->superAdmin()->create();
$superAdmin->generateUserApiKey();
$superAdmin->save();
$space1Accounts = $this->setSpaceOnRoute($space1, route('accounts.index'));
$space2Accounts = $this->setSpaceOnRoute($space2, route('accounts.index'));
$this->keyAuthenticated($superAdmin)
->json($this->method, $this->route, [
'username' => $username,
'domain' => $domain1,
'domain' => $space1->domain,
'admin' => true,
'algorithm' => 'SHA-256',
'password' => '123456',
])
->assertStatus(200)
->assertJson([
'username' => $username,
'domain' => $domain1
'domain' => $space1->domain
]);
$this->keyAuthenticated($account)
$this->keyAuthenticated($superAdmin)
->json($this->method, $this->route, [
'username' => $username,
'domain' => $domain2,
'domain' => $space2->domain,
'admin' => true,
'algorithm' => 'SHA-256',
'password' => '123456',
])
->assertStatus(200)
->assertJson([
'username' => $username,
'domain' => $domain2
'domain' => $space2->domain
]);
$this->keyAuthenticated($account)
->get($this->route)
config()->set('app.sip_domain', null);
$this->keyAuthenticated($superAdmin)
->get($space1Accounts)
->assertStatus(200)
->assertJson(['data' => [
->assertJson([
'data' => [
[
'username' => $account->username,
'domain' => $account->domain
'username' => $superAdmin->username,
'domain' => $superAdmin->domain
],
[
'username' => $username,
'domain' => $domain1
],
[
'username' => $username,
'domain' => $domain2
'domain' => $space1->domain
]
]]);
]
])
->assertJsonMissing([
'data' => [
[
'username' => $username,
'domain' => $space1->domain
]
]
]);
// Super admin on space 1
$admin1 = Account::where('username', $username)
->where('domain', $space1->domain)
->first();
$admin1->generateUserApiKey();
$this->keyAuthenticated($admin1)
->get($space1Accounts)
->assertStatus(200);
$this->keyAuthenticated($admin1)
->get($space2Accounts)
->assertStatus(200);
$this->keyAuthenticated($superAdmin)
->get($space2Accounts)
->assertStatus(200)
->assertJsonMissing([
'data' => [
[
'username' => $superAdmin->username,
'domain' => $superAdmin->domain
],
[
'username' => $username,
'domain' => $space1->domain
]
]
])
->assertJson([
'data' => [
[
'username' => $username,
'domain' => $space2->domain
]
]
]);
// Simple admin on space 2
$admin2 = Account::where('username', $username)
->where('domain', $space2->domain)
->first();
$admin2->generateUserApiKey();
$this->keyAuthenticated($admin2)
->get($space1Accounts)
->assertStatus(403);
$this->keyAuthenticated($admin2)
->get($space2Accounts)
->assertStatus(200);
}
public function testCreateDomainAsAdmin()
@ -795,9 +856,11 @@ class ApiAccountTest extends TestCase
->assertStatus(200)
->assertJson([
'username' => $account->username,
'passwords' => [[
'passwords' => [
[
'algorithm' => $algorithm
]]
]
]
]);
// Set new password without old one
@ -830,9 +893,11 @@ class ApiAccountTest extends TestCase
->assertStatus(200)
->assertJson([
'username' => $account->username,
'passwords' => [[
'passwords' => [
[
'algorithm' => $newAlgorithm
]]
]
]
]);
}

View file

@ -21,7 +21,6 @@ namespace Tests;
use App\PhoneCountry;
use App\Space;
use App\Http\Middleware\SpaceCheck;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;