Fix #125 Remove the External Accounts feature

This commit is contained in:
Timothée Jaussoin 2023-10-04 15:31:11 +02:00
parent 7feb7fd184
commit e996a9827c
19 changed files with 35 additions and 526 deletions

View file

@ -203,26 +203,6 @@ This command will set the admin role to any available Flexisip account. You need
Once one account is declared as administrator, you can directly configure the other ones using the web panel.
### Generate External Accounts
Generate `amount` accounts defined by the `group` label.
The generated accounts will have a random username suffixed by the group name.
accounts:generate-external {amount} {group}
### Export External Accounts
Export all the accounts defined by the `group` label.
The command generates a JSON file containing the accounts ready to by imported as External Accounts in the current directory. A specific path can be defined using the `--o|output` optional parameter.
accounts:export-to-externals {group} {--o|output=}
### Import External Accounts
Import accounts previously exported as a JSON file. Accounts previously imported will be skipped in the process.
accounts:import-externals {file_path}
## Custom email templaces
Some email templates can be customized.

View file

@ -77,21 +77,6 @@ class Account extends Authenticatable
$builder->where('domain', config('app.sip_domain'));
});
/**
* External account handling
*/
static::creating(function ($account) {
if (config('app.consume_external_account_on_create') && !getAvailableExternalAccount()) {
abort(403, 'Accounts cannot be created on the server');
}
});
static::created(function ($account) {
if (config('app.consume_external_account_on_create')) {
$account->attachExternalAccount();
}
});
}
public function scopeSip($query, string $sip)
@ -138,11 +123,6 @@ class Account extends Authenticatable
return $this->hasOne(ApiKey::class);
}
public function externalAccount()
{
return $this->hasOne(ExternalAccount::class);
}
public function contacts()
{
return $this->belongsToMany(Account::class, 'contacts', 'account_id', 'contact_id');
@ -284,17 +264,6 @@ class Account extends Authenticatable
return ($this->activationExpiration && $this->activationExpiration->isExpired());
}
public function attachExternalAccount(): bool
{
$externalAccount = getAvailableExternalAccount();
if (!$externalAccount) abort(403, 'No External Account left');
$externalAccount->account_id = $this->id;
$externalAccount->used = true;
return $externalAccount->save();
}
public function generateApiKey(): ApiKey
{
$this->apiKey()->delete();

View file

@ -1,51 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Account;
use Illuminate\Console\Command;
class ExportToExternalAccounts extends Command
{
protected $signature = 'accounts:export-to-externals {group} {--o|output=}';
protected $description = 'Export accounts from a group as external ones';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$accounts = Account::where('group', $this->argument('group'))
->with('passwords')
->get();
if ($accounts->count() == 0) {
$this->error('Nothing to export');
return;
}
$this->info('Exporting '.$accounts->count().' accounts');
$data = [];
foreach ($accounts as $account) {
array_push($data, [
'username' => $account->username,
'domain' => $account->domain,
'group' => $account->group,
'password' => $account->passwords->first()->password,
'algorithm' => $account->passwords->first()->algorithm,
]);
}
file_put_contents(
$this->option('output') ?? getcwd() . '/exported_accounts.json',
json_encode($data)
);
$this->info('Exported');
}
}

View file

@ -1,101 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Account;
use App\Password;
use App\Rules\NoUppercase;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class GenerateExternalAccounts extends Command
{
protected $signature = 'accounts:generate-external {amount} {group}';
protected $description = 'Generate external accounts in the designed group';
public function __construct()
{
parent::__construct();
}
public function handle()
{
$validator = Validator::make([
'amount' => $this->argument('amount'),
'group' => $this->argument('group'),
], [
'amount' => ['required', 'integer'],
'group' => ['required', 'alpha-dash', new NoUppercase]
]);
if ($validator->fails()) {
$this->info('External accounts no created:');
foreach ($validator->errors()->all() as $error) {
$this->error($error);
}
return 1;
}
$groups = Account::distinct('group')
->whereNotNull('group')
->get('group')
->pluck('group')
->toArray();
if (!in_array($this->argument('group'), $groups)) {
$this->info('Existing groups: '.implode(',', $groups));
if (!$this->confirm('You are creating a new group of External Account, are you sure?', false)) {
$this->info('Creation aborted');
return 0;
}
}
$accounts = collect();
$passwords = collect();
$algorithm = 'SHA-256';
$i = 0;
while ($i < $this->argument('amount')) {
$account = new Account;
$account->username = Str::random(12);
$account->domain = config('app.sip_domain');
$account->activated = 1;
$account->ip_address = '127.0.0.1';
$account->user_agent = 'External Account Generator';
$account->group = $this->argument('group');
$account->created_at = Carbon::now();
$i++;
$account->push($account->toArray());
}
Account::insert($accounts->toArray());
$insertedAccounts = Account::where('group', $this->argument('group'))
->latest()
->take($this->argument('amount'))
->get();
foreach ($insertedAccounts as $account) {
$password = new Password;
$password->account_id = $account->id;
$password->password = bchash($account->username, $account->resolvedRealm, Str::random(6), $algorithm);
$password->algorithm = $algorithm;
$passwords->push($password->only(['account_id', 'password', 'algorithm']));
}
Password::insert($passwords->toArray());
$this->info($this->argument('amount') . ' accounts created under the "' . $this->argument('group') . '" group');
return 0;
}
}

View file

@ -1,68 +0,0 @@
<?php
namespace App\Console\Commands;
use App\ExternalAccount;
use Illuminate\Console\Command;
class ImportExternalAccounts extends Command
{
protected $signature = 'accounts:import-externals {file_path}';
protected $description = 'Import external accounts from a file';
public function __construct()
{
parent::__construct();
}
public function handle()
{
if (!file_exists($this->argument('file_path'))) {
$this->error('The file does not exists');
return 1;
}
$json = json_decode(file_get_contents($this->argument('file_path')));
if (empty($json)) {
$this->error('Nothing to import or incorrect file');
return 1;
}
$existingUsernames = ExternalAccount::select('username')
->from('external_accounts')
->get()
->pluck('username');
$existingCounter = 0;
$importedCounter = 0;
$externalAccounts = collect();
foreach ($json as $account) {
if ($existingUsernames->contains($account->username)) {
$existingCounter++;
continue;
}
$externalAccount = new ExternalAccount;
$externalAccount->username = $account->username;
$externalAccount->domain = $account->domain;
$externalAccount->group = $account->group;
$externalAccount->password = $account->password;
$externalAccount->algorithm = $account->algorithm;
$externalAccounts->push($externalAccount->toArray());
$importedCounter++;
}
ExternalAccount::insert($externalAccounts->toArray());
$this->info($importedCounter . ' accounts imported');
if ($existingCounter > 0) {
$this->info($existingCounter . ' accounts already in the database');
}
return 0;
}
}

View file

@ -1,26 +0,0 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ExternalAccount extends Model
{
use HasFactory;
public function account()
{
return $this->belongsTo('App\Account');
}
public function getIdentifierAttribute()
{
return $this->attributes['username'].'@'.$this->attributes['domain'];
}
public function getResolvedRealmAttribute()
{
return $this->attributes['domain'];
}
}

View file

@ -21,7 +21,6 @@ use Illuminate\Support\Str;
use App\Account;
use App\DigestNonce;
use App\ExternalAccount;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
use League\CommonMark\CommonMarkConverter;
@ -87,28 +86,9 @@ function markdownDocumentationView($view): string
);
}
function getAvailableExternalAccount(): ?ExternalAccount
{
if (Schema::hasTable('external_accounts')) {
return ExternalAccount::where('used', false)
->where('account_id', null)
->first();
}
return null;
}
function publicRegistrationEnabled(): bool
{
if (config('app.public_registration')) {
if (config('app.consume_external_account_on_create')) {
return (bool)getAvailableExternalAccount();
}
return true;
}
return false;
return (config('app.public_registration'));
}
function isRegularExpression($string): bool

View file

@ -182,8 +182,6 @@ class ProvisioningController extends Controller
$config->appendChild($section);
if ($account) {
$externalAccount = $account->externalAccount;
$section = $dom->createElement('section');
$section->setAttribute('name', 'proxy_' . $proxyConfigIndex);
@ -204,12 +202,6 @@ class ProvisioningController extends Controller
provisioningProxyHook($section, $request, $account);
}
if ($externalAccount) {
$entry = $dom->createElement('entry', 'external_account');
$entry->setAttribute('name', 'depends_on');
$section->appendChild($entry);
}
$config->appendChild($section);
$passwords = $account->passwords()->get();
@ -249,55 +241,6 @@ class ProvisioningController extends Controller
}
$proxyConfigIndex++;
// External Account handling
if ($externalAccount) {
$section = $dom->createElement('section');
$section->setAttribute('name', 'proxy_' . $proxyConfigIndex);
$entry = $dom->createElement('entry', '<sip:' . $externalAccount->identifier . '>');
$entry->setAttribute('name', 'reg_identity');
$section->appendChild($entry);
$entry = $dom->createElement('entry', 1);
$entry->setAttribute('name', 'reg_sendregister');
$section->appendChild($entry);
$entry = $dom->createElement('entry', 'push_notification');
$entry->setAttribute('name', 'refkey');
$section->appendChild($entry);
$entry = $dom->createElement('entry', 'external_account');
$entry->setAttribute('name', 'idkey');
$section->appendChild($entry);
$config->appendChild($section);
$section = $dom->createElement('section');
$section->setAttribute('name', 'auth_info_' . $authInfoIndex);
$entry = $dom->createElement('entry', $externalAccount->username);
$entry->setAttribute('name', 'username');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $externalAccount->domain);
$entry->setAttribute('name', 'domain');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $externalAccount->password);
$entry->setAttribute('name', 'ha1');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $externalAccount->resolvedRealm);
$entry->setAttribute('name', 'realm');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $externalAccount->algorithm);
$entry->setAttribute('name', 'algorithm');
$section->appendChild($entry);
$config->appendChild($section);
}
}
// Complete the section with the Auth hook

View file

@ -26,7 +26,6 @@ use Carbon\Carbon;
use App\Account;
use App\ContactsList;
use App\ExternalAccount;
use App\Http\Requests\CreateAccountRequest;
use App\Http\Requests\UpdateAccountRequest;
@ -39,7 +38,7 @@ class AccountController extends Controller
'order_sort' => 'in:asc,desc',
]);
$accounts = Account::with('externalAccount', 'contactsLists')
$accounts = Account::with('contactsLists')
->orderBy($request->get('order_by', 'updated_at'), $request->get('order_sort', 'desc'));
if ($request->has('search')) {
@ -110,7 +109,6 @@ class AccountController extends Controller
return view('admin.account.create_edit', [
'account' => Account::findOrFail($id),
'protocols' => [null => 'None'] + Account::$dtmfProtocols,
'external_accounts_count' => ExternalAccount::where('used', false)->count(),
'contacts_lists' => ContactsList::whereNotIn('id', function ($query) use ($id) {
$query->select('contacts_list_id')
->from('account_contacts_list')
@ -145,16 +143,6 @@ class AccountController extends Controller
return redirect()->route('admin.account.edit', $id);
}
public function attachExternalAccount(int $id)
{
$account = Account::findOrFail($id);
$account->attachExternalAccount();
Log::channel('events')->info('Web Admin: ExternalAccount attached', ['id' => $account->identifier]);
return redirect()->back();
}
public function provision(int $id)
{
$account = Account::findOrFail($id);

View file

@ -28,8 +28,7 @@ class ContactsListContactController extends Controller
{
public function add(Request $request, int $contactsListId)
{
$accounts = Account::orderBy('updated_at', $request->get('updated_at_order', 'desc'))
->with('externalAccount');
$accounts = Account::orderBy('updated_at', $request->get('updated_at_order', 'desc'));
if ($request->has('search')) {
$accounts = $accounts->where('username', 'like', '%' . $request->get('search') . '%');

View file

@ -29,7 +29,6 @@ return [
'transport_protocol_text' => env('ACCOUNT_TRANSPORT_PROTOCOL_TEXT', 'TLS (recommended), TCP or UDP'),
'account_email_unique' => env('ACCOUNT_EMAIL_UNIQUE', false),
'consume_external_account_on_create' => env('ACCOUNT_CONSUME_EXTERNAL_ACCOUNT_ON_CREATE', false),
'blacklisted_usernames' => env('ACCOUNT_BLACKLISTED_USERNAMES', ''),
'account_username_regex' => env('ACCOUNT_USERNAME_REGEX', '^[a-z0-9+_.-]*$'),

View file

@ -1,25 +0,0 @@
<?php
namespace Database\Factories;
use App\ExternalAccount;
use Illuminate\Database\Eloquent\Factories\Factory;
class ExternalAccountFactory extends Factory
{
protected $model = ExternalAccount::class;
public function definition()
{
$username = $this->faker->username;
$realm = config('app.realm') ?? config('app.sip_domain');
return [
'username' => $username,
'domain' => config('app.sip_domain'),
'group' => 'test',
'password' => hash('sha256', $username.':'.$realm.':testtest'),
'algorithm' => 'SHA-256',
];
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::dropIfExists('external_accounts');
}
public function down()
{
Schema::create('external_accounts', function (Blueprint $table) {
$table->id();
$table->string('username', 64);
$table->string('domain', 64);
$table->string('group', 16);
$table->string('password', 255);
$table->string('algorithm', 10)->default('MD5');
$table->boolean('used')->default(false);
$table->integer('account_id')->unsigned()->nullable();
$table->foreign('account_id')->references('id')
->on('accounts')->onDelete('set null');
$table->timestamps();
});
}
};

View file

@ -100,14 +100,6 @@ Administrators can create and edit accounts directly from the admin panel. Durin
The deletion of an account is definitive, all the database related data (password, aliases…) will be destroyed after the deletion.
### Attach the account to an External Account
@if (config('app.consume_external_account_on_create') == false)
*The feature is not enabled on this instance.*
@endif
It is possible to import external accounts in the application. If this feature is enabled those External Accounts can be attached during the account creation process or afterward using the Account page dedicated button.
### Create, edit and delete account types
An adminisator can create, edit and delete account types. Those can be used to categorize accounts in clients, they are often used for Internet of Things related devices.

View file

@ -203,18 +203,6 @@
</p>
@endif
<h2>External Account</h2>
@if ($account->externalAccount)
<p>
<b>Identifier:</b> {{ $account->externalAccount->identifier }}<br />
</p>
@else
<a class="btn @if ($external_accounts_count == 0) disabled @endif"
href="{{ route('admin.account.external_account.attach', $account->id) }}">Attach an External Account
({{ $external_accounts_count }} left)</a>
@endif
<h2>Actions</h2>
@if ($account->dtmf_protocol)

View file

@ -98,9 +98,6 @@
@endif
</td>
<td>
@if ($account->externalAccount)
<span class="badge badge-secondary" title="External Account attached">EA</span>
@endif
@if ($account->email)
<span class="badge badge-info">Email</span>
@endif

View file

@ -21,7 +21,6 @@ The general idea is to allow the clients to access a unique URL returning a cust
* <span class="badge badge-success">Public</span> Expose the linphonerc INI file configuration
* <span class="badge badge-info">User</span> Inject the authentication information to allow the application to authenticate on the server directly if a valid account is detected using the `provisioning` token
* A similar information is also injected if an external account is linked to the main one, the application will then be able to authenticate on both accounts at the same time
* <span class="badge badge-success">Public</span> <span class="badge badge-info">User</span> Using __Custom Hooks__ an admin is also able to have access to the authenticated User internal object and inject custom XML during the provisioning. See the specific section in the `README.md` to learn more about that feature.
### Features

View file

@ -138,8 +138,6 @@ if (config('app.web_panel')) {
Route::name('account.')->prefix('accounts')->group(function () {
Route::controller(AdminAccountController::class)->group(function () {
Route::get('{account_id}/external_account/attach', 'attachExternalAccount')->name('external_account.attach');
Route::get('{account_id}/provision', 'provision')->name('provision');
Route::get('create', 'create')->name('create');

View file

@ -1,84 +0,0 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Tests\Feature;
use App\Admin;
use App\Account;
use App\ExternalAccount;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ApiExternalAccountTest extends TestCase
{
use RefreshDatabase;
protected $route = '/api/accounts';
protected $provisioningRoute = '/provisioning/me';
protected $method = 'POST';
public function testExternalAccountAttachOnCreate()
{
$admin = Admin::factory()->create();
$password = $admin->account->passwords()->first();
$password->account->generateApiKey();
$password->account->save();
config()->set('app.consume_external_account_on_create', true);
// Seed an ExternalAccount
$externalAccount = ExternalAccount::factory()->create();
$externalAccount->save();
$response = $this->keyAuthenticated($password->account)
->json($this->method, $this->route, [
'username' => 'test',
'domain' => 'example.com',
'algorithm' => 'SHA-256',
'password' => '123456',
'activated' => true,
]);
$response->assertStatus(200);
// No ExternalAccount left
$response = $this->keyAuthenticated($password->account)
->json($this->method, $this->route, [
'username' => 'test2',
'domain' => 'example.com',
'algorithm' => 'SHA-256',
'password' => '123456',
]);
$response->assertStatus(403);
$createdAccount = Account::where('username', 'test')->first();
$createdAccount->generateApiKey();
$createdAccount->save();
$response = $this->keyAuthenticated($createdAccount)
->get($this->provisioningRoute)
->assertStatus(200)
->assertHeader('Content-Type', 'application/xml')
->assertSee($externalAccount->identifier)
->assertSee('ha1')
->assertSee('idkey')
->assertSee('depends_on');
}
}