Add ExternalAccounts and related features

- Consume an ExternalAccount on Account creation
- Add a tombstone to an ExternalAccount to ensure non re-usage
- Add related tests
- Generalize Utils
- Stop public registration when there is no ExternalAccounts left
- Add GenerateExternalAccounts, ExportToExternalAccounts and ImportExternalAccounts console scripts
- Provision the ExternalAccount using the depends_on/idkey pair
This commit is contained in:
Timothée Jaussoin 2022-06-29 16:19:37 +02:00
parent 093619a22c
commit 7a17897193
35 changed files with 694 additions and 227 deletions

View file

@ -20,6 +20,9 @@ ACCOUNT_PROXY_REGISTRAR_ADDRESS=sip.example.com # Proxy registrar address, can b
ACCOUNT_TRANSPORT_PROTOCOL_TEXT="TLS (recommended), TCP or UDP" # Simple text, to explain how the SIP server can be reached
ACCOUNT_REALM=null # Default realm for the accounts, fallback to the domain if not set, enforce null by default
# Account creation
ACCOUNT_CONSUME_EXTERNAL_ACCOUNT_ON_CREATE=false
# Account provisioning
ACCOUNT_PROVISIONING_RC_FILE=
ACCOUNT_PROVISIONING_OVERWRITE_ALL=

View file

@ -179,6 +179,26 @@ This command will set the admin role to any available Flexisip account (the exte
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}
## Provisioning
FlexiAPI is providing endpoints to provision Liblinphone powered devices. You can find more documentation about it on the `/api#provisioning` documentation page.

View file

@ -29,7 +29,6 @@ use Illuminate\Support\Str;
use App\ApiKey;
use App\Password;
use App\EmailChanged;
use App\Helpers\Utils;
use App\Mail\ChangingEmail;
use Carbon\Carbon;
@ -63,6 +62,21 @@ 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)
@ -109,6 +123,11 @@ class Account extends Authenticatable
return $this->hasOne('App\ApiKey');
}
public function externalAccount()
{
return $this->hasOne('App\ExternalAccount');
}
public function contacts()
{
return $this->belongsToMany('App\Account', 'contacts', 'account_id', 'contact_id');
@ -203,6 +222,17 @@ 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 requestEmailUpdate(string $newEmail)
{
// Remove all the old requests
@ -272,7 +302,7 @@ class Account extends Authenticatable
$password = new Password;
$password->account_id = $this->id;
$password->password = Utils::bchash($this->username, $this->resolvedRealm, $newPassword, $algorithm);
$password->password = bchash($this->username, $this->resolvedRealm, $newPassword, $algorithm);
$password->algorithm = $algorithm;
$password->save();
}

View file

@ -0,0 +1,51 @@
<?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

@ -0,0 +1,65 @@
<?php
namespace App\Console\Commands;
use App\Account;
use App\Password;
use Carbon\Carbon;
use Illuminate\Console\Command;
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()
{
$accounts = collect();
$passwords = collect();
$algorithm = 'SHA-256';
$i = 0;
while ($i < $this->argument('amount')) {
$account = new Account;
$account->username = $this->argument('group') . '_' . Str::random(6);
$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->creation_time = Carbon::now();
$i++;
$account->push($account->toArray());
}
Account::insert($accounts->toArray());
$insertedAccounts = Account::where('group', $this->argument('group'))
->orderBy('creation_time', 'desc')
->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

@ -0,0 +1,67 @@
<?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++;
} else {
$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

@ -0,0 +1,26 @@
<?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 config('app.realm') ?? $this->attributes['domain'];
}
}

View file

@ -17,77 +17,97 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace App\Helpers;
use Illuminate\Support\Str;
use App\Account;
use App\DigestNonce;
use App\ExternalAccount;
use Illuminate\Support\Facades\Schema;
use League\CommonMark\Environment;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
use League\CommonMark\MarkdownConverter;
class Utils
function generateNonce(): string
{
public static function generateNonce(): string
{
return Str::random(32);
}
public static function generateValidNonce(Account $account): string
{
$nonce = new DigestNonce;
$nonce->account_id = $account->id;
$nonce->nonce = Utils::generateNonce();
$nonce->save();
return $nonce->nonce;
}
public static function bchash(string $username, string $domain, string $password, string $algorithm = 'MD5')
{
$algos = ['MD5' => 'md5', 'SHA-256' => 'sha256'];
return hash($algos[$algorithm], $username.':'.$domain.':'.$password);
}
public static function generatePin()
{
return mt_rand(1000, 9999);
}
public static function percent($value, $max)
{
if ($max == 0) $max = 1;
return round(($value*100)/$max, 2);
}
public static function markdownDocumentationView($view)
{
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new HeadingPermalinkExtension);
$environment->addExtension(new TableOfContentsExtension);
$environment->mergeConfig([
'heading_permalink' => [
'html_class' => 'permalink',
'insert' => 'after',
'title' => 'Permalink',
'id_prefix' => '',
'fragment_prefix' => '',
],
'table_of_contents' => [
'html_class' => 'table-of-contents float-right',
],
]);
$converter = new MarkdownConverter($environment);
return (string) $converter->convertToHtml(
view($view, [
'app_name' => config('app.name')
])->render()
);
}
return Str::random(32);
}
function generateValidNonce(Account $account): string
{
$nonce = new DigestNonce;
$nonce->account_id = $account->id;
$nonce->nonce = generateNonce();
$nonce->save();
return $nonce->nonce;
}
function bchash(string $username, string $domain, string $password, string $algorithm = 'MD5')
{
$algos = ['MD5' => 'md5', 'SHA-256' => 'sha256'];
return hash($algos[$algorithm], $username.':'.$domain.':'.$password);
}
function generatePin()
{
return mt_rand(1000, 9999);
}
function percent($value, $max)
{
if ($max == 0) $max = 1;
return round(($value*100)/$max, 2);
}
function markdownDocumentationView($view): string
{
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new HeadingPermalinkExtension);
$environment->addExtension(new TableOfContentsExtension);
$environment->mergeConfig([
'heading_permalink' => [
'html_class' => 'permalink',
'insert' => 'after',
'title' => 'Permalink',
'id_prefix' => '',
'fragment_prefix' => '',
],
'table_of_contents' => [
'html_class' => 'table-of-contents float-right',
],
]);
$converter = new MarkdownConverter($environment);
return (string) $converter->convertToHtml(
view($view, [
'app_name' => config('app.name')
])->render()
);
}
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;
}

View file

@ -25,7 +25,6 @@ use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
use App\Account;
use App\AccountTombstone;
use App\Helpers\Utils;
class AccountController extends Controller
{
@ -43,7 +42,7 @@ class AccountController extends Controller
public function documentation(Request $request)
{
return view('account.documentation', [
'documentation' => Utils::markdownDocumentationView('account.documentation_markdown')
'documentation' => markdownDocumentationView('account.documentation_markdown')
]);
}

View file

@ -28,7 +28,6 @@ use Illuminate\Support\Facades\Mail;
use App\Account;
use App\Alias;
use App\AuthToken;
use App\Helpers\Utils;
use App\Libraries\OvhSMS;
use App\Mail\PasswordAuthentication;
@ -72,7 +71,7 @@ class AuthenticateController extends Controller
foreach ($account->passwords as $password) {
if (hash_equals(
$password->password,
Utils::bchash($account->username, $account->resolvedRealm, $request->get('password'), $password->algorithm)
bchash($account->username, $account->resolvedRealm, $request->get('password'), $password->algorithm)
)) {
Auth::login($account);
return redirect()->route('account.panel');
@ -208,7 +207,7 @@ class AuthenticateController extends Controller
]);
}
$account->confirmation_key = Utils::generatePin();
$account->confirmation_key = generatePin();
$account->save();
$ovhSMS = new OvhSMS;

View file

@ -24,7 +24,6 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use App\Helpers\Utils;
use App\Mail\ConfirmedRegistration;
class PasswordController extends Controller

View file

@ -143,10 +143,12 @@ class ProvisioningController extends Controller
$config->appendChild($section);
if ($account && !$account->activationExpired()) {
$externalAccount = $account->externalAccount;
$section = $dom->createElement('section');
$section->setAttribute('name', 'proxy_' . $proxyConfigIndex);
$entry = $dom->createElement('entry', '<sip:'.$account->identifier.'>');
$entry = $dom->createElement('entry', '<sip:' . $account->identifier . '>');
$entry->setAttribute('name', 'reg_identity');
$section->appendChild($entry);
@ -163,6 +165,12 @@ 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();
@ -199,19 +207,71 @@ class ProvisioningController extends Controller
$config->appendChild($section);
$authInfoIndex++;
}
if ($provisioningToken) {
// Activate the account
if ($account->activated == false
&& $provisioningToken == $account->provisioning_token) {
if (
$account->activated == false
&& $provisioningToken == $account->provisioning_token
) {
$account->activated = true;
}
$account->provisioning_token = null;
$account->save();
}
$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', $account->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

@ -32,7 +32,6 @@ use App\Alias;
use App\Rules\WithoutSpaces;
use App\Rules\IsNotPhoneNumber;
use App\Rules\NoUppercase;
use App\Helpers\Utils;
use App\Libraries\OvhSMS;
use App\Mail\RegisterConfirmation;
use App\Mail\NewsletterRegistration;
@ -155,7 +154,7 @@ class RegisterController extends Controller
$alias->account_id = $account->id;
$alias->save();
$account->confirmation_key = Utils::generatePin();
$account->confirmation_key = generatePin();
$account->save();
$ovhSMS = new OvhSMS;

View file

@ -28,6 +28,7 @@ use Carbon\Carbon;
use App\Account;
use App\Admin;
use App\Alias;
use App\ExternalAccount;
use App\Http\Requests\CreateAccountRequest;
use App\Http\Requests\UpdateAccountRequest;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
@ -36,7 +37,7 @@ class AccountController extends Controller
{
public function index(Request $request, $search = '')
{
$accounts = Account::orderBy('creation_time', 'desc');
$accounts = Account::orderBy('creation_time', 'desc')->with('externalAccount');
if (!empty($search)) {
$accounts = $accounts->where('username', 'like', '%'.$search.'%');
@ -51,6 +52,7 @@ class AccountController extends Controller
public function show(int $id)
{
return view('admin.account.show', [
'external_accounts_count' => ExternalAccount::where('used', false)->count(),
'account' => Account::findOrFail($id)
]);
}
@ -116,6 +118,16 @@ class AccountController extends Controller
return redirect()->route('admin.account.index', $request->get('search'));
}
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 activate(int $id)
{
$account = Account::findOrFail($id);

View file

@ -24,7 +24,6 @@ use Illuminate\Support\Facades\Log;
use App\Http\Controllers\Controller;
use App\Rules\WithoutSpaces;
use App\Helpers\Utils;
use App\Libraries\OvhSMS;
use App\PhoneChangeCode;
@ -47,7 +46,7 @@ class AccountPhoneController extends Controller
$phoneChangeCode = $account->phoneChangeCode ?? new PhoneChangeCode;
$phoneChangeCode->account_id = $account->id;
$phoneChangeCode->phone = $request->get('phone');
$phoneChangeCode->code = Utils::generatePin();
$phoneChangeCode->code = generatePin();
$phoneChangeCode->save();
Log::channel('events')->info('API: Account phone change requested by SMS', ['id' => $account->identifier]);

View file

@ -19,7 +19,6 @@
namespace App\Http\Controllers\Api;
use App\Helpers\Utils;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
@ -28,7 +27,7 @@ class ApiController extends Controller
public function documentation(Request $request)
{
return view('api.documentation', [
'documentation' => Utils::markdownDocumentationView('api.documentation_markdown')
'documentation' => markdownDocumentationView('api.documentation_markdown')
]);
}
}

View file

@ -24,7 +24,6 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use App\Helpers\Utils;
use App\Mail\ConfirmedRegistration;
class PasswordController extends Controller
@ -48,7 +47,7 @@ class PasswordController extends Controller
foreach ($account->passwords as $password) {
if (hash_equals(
$password->password,
Utils::bchash($account->username, $account->resolvedRealm, $request->get('old_password'), $password->algorithm)
bchash($account->username, $account->resolvedRealm, $request->get('old_password'), $password->algorithm)
)) {
$account->updatePassword($request->get('password'), $algorithm);

View file

@ -20,7 +20,6 @@
namespace App\Http\Middleware;
use App\Account;
use App\Helpers\Utils;
use Carbon\Carbon;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
@ -170,7 +169,7 @@ class AuthenticateDigestOrKey
{
$response = new Response;
$nonce = Utils::generateValidNonce($account);
$nonce = generateValidNonce($account);
$headers = $this->generateAuthHeaders($account, $nonce);
if (!empty($headers)) {

View file

@ -27,6 +27,7 @@ class Password extends Model
use HasFactory;
public $timestamps = false;
protected $fillable = ['account_id', 'password', 'algorithm'];
protected $hidden = ['id', 'password', 'account_id', 'created_at', 'updated_at'];
public function account()

View file

@ -0,0 +1,18 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class HelperServiceProvider extends ServiceProvider
{
public function register()
{
require_once app_path('Helpers/Utils.php');
}
public function boot()
{
//
}
}

185
flexiapi/composer.lock generated
View file

@ -1468,16 +1468,16 @@
},
{
"name": "laravel/framework",
"version": "v8.83.17",
"version": "v8.83.18",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "2cf142cd5100b02da248acad3988bdaba5635e16"
"reference": "db8188e9cc8359a5c6706fa9d9f55aad7f235077"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/2cf142cd5100b02da248acad3988bdaba5635e16",
"reference": "2cf142cd5100b02da248acad3988bdaba5635e16",
"url": "https://api.github.com/repos/laravel/framework/zipball/db8188e9cc8359a5c6706fa9d9f55aad7f235077",
"reference": "db8188e9cc8359a5c6706fa9d9f55aad7f235077",
"shasum": ""
},
"require": {
@ -1637,7 +1637,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2022-06-21T14:38:31+00:00"
"time": "2022-06-28T14:30:38+00:00"
},
{
"name": "laravel/serializable-closure",
@ -2187,16 +2187,16 @@
},
{
"name": "nesbot/carbon",
"version": "2.58.0",
"version": "2.59.1",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "97a34af22bde8d0ac20ab34b29d7bfe360902055"
"reference": "a9000603ea337c8df16cc41f8b6be95a65f4d0f5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/97a34af22bde8d0ac20ab34b29d7bfe360902055",
"reference": "97a34af22bde8d0ac20ab34b29d7bfe360902055",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/a9000603ea337c8df16cc41f8b6be95a65f4d0f5",
"reference": "a9000603ea337c8df16cc41f8b6be95a65f4d0f5",
"shasum": ""
},
"require": {
@ -2211,11 +2211,12 @@
"doctrine/orm": "^2.7",
"friendsofphp/php-cs-fixer": "^3.0",
"kylekatarnls/multi-tester": "^2.0",
"ondrejmirtes/better-reflection": "*",
"phpmd/phpmd": "^2.9",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12.54 || ^1.0",
"phpunit/php-file-iterator": "^2.0.5",
"phpunit/phpunit": "^7.5.20 || ^8.5.23",
"phpstan/phpstan": "^0.12.99 || ^1.7.14",
"phpunit/php-file-iterator": "^2.0.5 || ^3.0.6",
"phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20",
"squizlabs/php_codesniffer": "^3.4"
},
"bin": [
@ -2272,15 +2273,19 @@
},
"funding": [
{
"url": "https://opencollective.com/Carbon",
"type": "open_collective"
"url": "https://github.com/sponsors/kylekatarnls",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/nesbot/carbon",
"url": "https://opencollective.com/Carbon#sponsor",
"type": "opencollective"
},
{
"url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme",
"type": "tidelift"
}
],
"time": "2022-04-25T19:31:17+00:00"
"time": "2022-06-29T21:43:55+00:00"
},
{
"name": "nikic/php-parser",
@ -2878,16 +2883,16 @@
},
{
"name": "psy/psysh",
"version": "v0.11.5",
"version": "v0.11.7",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
"reference": "c23686f9c48ca202710dbb967df8385a952a2daf"
"reference": "77fc7270031fbc28f9a7bea31385da5c4855cb7a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/c23686f9c48ca202710dbb967df8385a952a2daf",
"reference": "c23686f9c48ca202710dbb967df8385a952a2daf",
"url": "https://api.github.com/repos/bobthecow/psysh/zipball/77fc7270031fbc28f9a7bea31385da5c4855cb7a",
"reference": "77fc7270031fbc28f9a7bea31385da5c4855cb7a",
"shasum": ""
},
"require": {
@ -2948,9 +2953,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
"source": "https://github.com/bobthecow/psysh/tree/v0.11.5"
"source": "https://github.com/bobthecow/psysh/tree/v0.11.7"
},
"time": "2022-05-27T18:03:49+00:00"
"time": "2022-07-07T13:49:11+00:00"
},
{
"name": "ralouphie/getallheaders",
@ -4099,7 +4104,7 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v2.5.1",
"version": "v2.5.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
@ -4146,7 +4151,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1"
"source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2"
},
"funding": [
{
@ -4322,7 +4327,7 @@
},
{
"name": "symfony/event-dispatcher-contracts",
"version": "v2.5.1",
"version": "v2.5.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher-contracts.git",
@ -4381,7 +4386,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.1"
"source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.2"
},
"funding": [
{
@ -5701,16 +5706,16 @@
},
{
"name": "symfony/service-contracts",
"version": "v2.5.1",
"version": "v2.5.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c"
"reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c",
"reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c",
"reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c",
"shasum": ""
},
"require": {
@ -5764,7 +5769,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/v2.5.1"
"source": "https://github.com/symfony/service-contracts/tree/v2.5.2"
},
"funding": [
{
@ -5780,7 +5785,7 @@
"type": "tidelift"
}
],
"time": "2022-03-13T20:07:29+00:00"
"time": "2022-05-30T19:17:29+00:00"
},
{
"name": "symfony/string",
@ -5967,16 +5972,16 @@
},
{
"name": "symfony/translation-contracts",
"version": "v2.5.1",
"version": "v2.5.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation-contracts.git",
"reference": "1211df0afa701e45a04253110e959d4af4ef0f07"
"reference": "136b19dd05cdf0709db6537d058bcab6dd6e2dbe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/1211df0afa701e45a04253110e959d4af4ef0f07",
"reference": "1211df0afa701e45a04253110e959d4af4ef0f07",
"url": "https://api.github.com/repos/symfony/translation-contracts/zipball/136b19dd05cdf0709db6537d058bcab6dd6e2dbe",
"reference": "136b19dd05cdf0709db6537d058bcab6dd6e2dbe",
"shasum": ""
},
"require": {
@ -6025,7 +6030,7 @@
"standards"
],
"support": {
"source": "https://github.com/symfony/translation-contracts/tree/v2.5.1"
"source": "https://github.com/symfony/translation-contracts/tree/v2.5.2"
},
"funding": [
{
@ -6041,7 +6046,7 @@
"type": "tidelift"
}
],
"time": "2022-01-02T09:53:40+00:00"
"time": "2022-06-27T16:58:25+00:00"
},
{
"name": "symfony/var-dumper",
@ -6401,30 +6406,29 @@
"packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
"version": "v3.6.7",
"version": "v3.7.0",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
"reference": "b96f9820aaf1ff9afe945207883149e1c7afb298"
"reference": "3372ed65e6d2039d663ed19aa699956f9d346271"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/b96f9820aaf1ff9afe945207883149e1c7afb298",
"reference": "b96f9820aaf1ff9afe945207883149e1c7afb298",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/3372ed65e6d2039d663ed19aa699956f9d346271",
"reference": "3372ed65e6d2039d663ed19aa699956f9d346271",
"shasum": ""
},
"require": {
"illuminate/routing": "^6|^7|^8|^9",
"illuminate/session": "^6|^7|^8|^9",
"illuminate/support": "^6|^7|^8|^9",
"illuminate/routing": "^7|^8|^9",
"illuminate/session": "^7|^8|^9",
"illuminate/support": "^7|^8|^9",
"maximebf/debugbar": "^1.17.2",
"php": ">=7.2",
"symfony/debug": "^4.3|^5|^6",
"symfony/finder": "^4.3|^5|^6"
"php": ">=7.2.5",
"symfony/finder": "^5|^6"
},
"require-dev": {
"mockery/mockery": "^1.3.3",
"orchestra/testbench-dusk": "^4|^5|^6|^7",
"orchestra/testbench-dusk": "^5|^6|^7",
"phpunit/phpunit": "^8.5|^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
@ -6470,7 +6474,7 @@
],
"support": {
"issues": "https://github.com/barryvdh/laravel-debugbar/issues",
"source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.6.7"
"source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.7.0"
},
"funding": [
{
@ -6482,7 +6486,7 @@
"type": "github"
}
],
"time": "2022-02-09T07:52:32+00:00"
"time": "2022-07-11T09:26:42+00:00"
},
{
"name": "doctrine/instantiator",
@ -6621,16 +6625,16 @@
},
{
"name": "facade/ignition",
"version": "2.17.5",
"version": "2.17.6",
"source": {
"type": "git",
"url": "https://github.com/facade/ignition.git",
"reference": "1d71996f83c9a5a7807331b8986ac890352b7a0c"
"reference": "6acd82e986a2ecee89e2e68adfc30a1936d1ab7c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/facade/ignition/zipball/1d71996f83c9a5a7807331b8986ac890352b7a0c",
"reference": "1d71996f83c9a5a7807331b8986ac890352b7a0c",
"url": "https://api.github.com/repos/facade/ignition/zipball/6acd82e986a2ecee89e2e68adfc30a1936d1ab7c",
"reference": "6acd82e986a2ecee89e2e68adfc30a1936d1ab7c",
"shasum": ""
},
"require": {
@ -6695,7 +6699,7 @@
"issues": "https://github.com/facade/ignition/issues",
"source": "https://github.com/facade/ignition"
},
"time": "2022-02-23T18:31:24+00:00"
"time": "2022-06-30T18:26:59+00:00"
},
{
"name": "facade/ignition-contracts",
@ -8933,75 +8937,6 @@
],
"time": "2020-09-28T06:39:44+00:00"
},
{
"name": "symfony/debug",
"version": "v4.4.41",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "6637e62480b60817b9a6984154a533e8e64c6bd5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/6637e62480b60817b9a6984154a533e8e64c6bd5",
"reference": "6637e62480b60817b9a6984154a533e8e64c6bd5",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"psr/log": "^1|^2|^3"
},
"conflict": {
"symfony/http-kernel": "<3.4"
},
"require-dev": {
"symfony/http-kernel": "^3.4|^4.0|^5.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Debug\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides tools to ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/debug/tree/v4.4.41"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"abandoned": "symfony/error-handler",
"time": "2022-04-12T15:19:55+00:00"
},
{
"name": "theseer/tokenizer",
"version": "1.2.1",

View file

@ -28,6 +28,8 @@ return [
'proxy_registrar_address' => env('ACCOUNT_PROXY_REGISTRAR_ADDRESS', 'sip.domain.com'),
'transport_protocol_text' => env('ACCOUNT_TRANSPORT_PROTOCOL_TEXT', 'TLS (recommended), TCP or UDP'),
'consume_external_account_on_create' => env('ACCOUNT_CONSUME_EXTERNAL_ACCOUNT_ON_CREATE', false),
/**
* Time limit before the API Key and related cookie are expired
*/
@ -224,6 +226,7 @@ return [
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\HelperServiceProvider::class,
],
@ -273,7 +276,7 @@ return [
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Utils' => App\Helpers\Utils::class,
//'Utils' => App\Helpers\class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
],

View file

@ -0,0 +1,25 @@
<?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

@ -19,7 +19,6 @@
namespace Database\Factories;
use App\Helpers\Utils;
use App\Password;
use App\PhoneChangeCode;
use Illuminate\Database\Eloquent\Factories\Factory;
@ -35,7 +34,7 @@ class PhoneChangeCodeFactory extends Factory
return [
'account_id' => $password->account->id,
'code' => Utils::generatePin(),
'code' => generatePin(),
'phone' => '+3312341234',
];
}

View file

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateExternalAccountsTable extends Migration
{
public function up()
{
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();
});
Schema::table('accounts', function (Blueprint $table) {
$table->string('group')->nullable();
$table->index('group');
});
}
public function down()
{
Schema::table('accounts', function (Blueprint $table) {
$table->dropIndex('accounts_group_index');
$table->dropColumn('group');
});
Schema::dropIfExists('external_accounts');
}
}

View file

@ -14,7 +14,7 @@
<hr />
<div class="list-group mb-3">
@if (config('app.public_registration'))
@if (publicRegistrationEnabled())
<a href="{{ route('account.register') }}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Create an account</h5>

View file

@ -28,7 +28,7 @@
@include('parts.password_recovery')
@endif
@if (config('app.public_registration'))
@if (publicRegistrationEnabled())
<hr />
<p class="text-center">

View file

@ -42,14 +42,17 @@
</a>
</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
@if ($account->activated)
<span class="badge badge-success">Activated</span>
<span class="badge badge-success" title="Activated">Act.</span>
@endif
@if ($account->admin)
<span class="badge badge-primary">Admin</span>
<span class="badge badge-primary" title="Admin">Adm.</span>
@endif
@if ($account->sha256Password)
<span class="badge badge-info">SHA256</span>

View file

@ -22,6 +22,7 @@
<b>Email:</b> <a href="mailto:{{ $account->email }}">{{ $account->email }}</a><br />
<b>DTMF Protocol:</b> @if ($account->dtmf_protocol) {{ $account->resolvedDtmfProtocol }}@endif<br />
@if ($account->alias)<b>Phone number:</b> {{ $account->phone }}<br />@endif
@if ($account->group)<b>Group:</b> {{ $account->group }}<br />@endif
@if ($account->display_name)<b>Display name:</b> {{ $account->display_name }}<br />@endif
</p>
@ -45,6 +46,16 @@
<span class="badge badge-danger">Not Admin</span> <a href="{{ route('admin.account.admin', $account->id) }}">Add admin role</a>
@endif
<h3 class="mt-3">External Account</h3>
@if ($account->externalAccount)
<p>
<b>Identifier:</b> {{ $account->externalAccount->identifier }}<br />
</p>
@else
<a class="btn btn-sm @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
<h3 class="mt-3">Contacts</h3>
<table class="table">

View file

@ -1,12 +1,12 @@
<div class="bar first" style="flex-basis: {{ Utils::percent($slice['phone'] - $slice['activated_phone'], $max) }}%"
<div class="bar first" style="flex-basis: {{ percent($slice['phone'] - $slice['activated_phone'], $max) }}%"
data-value="{{ $slice['phone'] - $slice['activated_phone'] }}"
title="Unactivated phone: {{ $slice['phone'] - $slice['activated_phone'] }}"></div>
<div class="bar first activated" style="flex-basis: {{ Utils::percent($slice['activated_phone'], $max) }}%"
<div class="bar first activated" style="flex-basis: {{ percent($slice['activated_phone'], $max) }}%"
data-value="{{ $slice['activated_phone'] }}"
title="Activated phone: {{ $slice['activated_phone'] }}"></div>
<div class="bar second" style="flex-basis: {{ Utils::percent($slice['email'] - $slice['activated_email'], $max) }}%"
<div class="bar second" style="flex-basis: {{ percent($slice['email'] - $slice['activated_email'], $max) }}%"
data-value="{{ $slice['email'] - $slice['activated_email'] }}"
title="Unactivated email: {{ $slice['email'] - $slice['activated_email'] }}"></div>
<div class="bar second activated" style="flex-basis: {{ Utils::percent($slice['activated_email'], $max) }}%"
<div class="bar second activated" style="flex-basis: {{ percent($slice['activated_email'], $max) }}%"
data-value="{{ $slice['activated_email'] }}"
title="Activated email: {{ $slice['activated_email'] }}"></div>

View file

@ -17,7 +17,7 @@
</ul>
<ul class="navbar-nav">
@if (config('app.public_registration'))
@if (publicRegistrationEnabled())
<li class="nav-item @if (request()->routeIs('account.register')) active @endif">
<a class="nav-link" href="{{ route('account.register') }}">Register</a>
</li>

View file

@ -48,7 +48,7 @@ Route::get('provisioning/auth_token/{auth_token}', 'Account\ProvisioningControll
Route::get('provisioning/qrcode/{provisioning_token}', 'Account\ProvisioningController@qrcode')->name('provisioning.qrcode');
Route::get('provisioning/{provisioning_token?}', 'Account\ProvisioningController@show')->name('provisioning.show');
if (config('app.public_registration')) {
if (publicRegistrationEnabled()) {
if (config('app.phone_authentication')) {
Route::get('register/phone', 'Account\RegisterController@registerPhone')->name('account.register.phone');
Route::post('register/phone', 'Account\RegisterController@storePhone')->name('account.store.phone');
@ -118,6 +118,8 @@ if (config('app.web_panel')) {
Route::get('admin/accounts/{account}/activate', 'Admin\AccountController@activate')->name('admin.account.activate');
Route::get('admin/accounts/{account}/deactivate', 'Admin\AccountController@deactivate')->name('admin.account.deactivate');
Route::get('admin/accounts/{account}/external_account/attach', 'Admin\AccountController@attachExternalAccount')->name('admin.account.external_account.attach');
Route::get('admin/accounts/{account}/admin', 'Admin\AccountController@admin')->name('admin.account.admin');
Route::get('admin/accounts/{id}/unadmin', 'Admin\AccountController@unadmin')->name('admin.account.unadmin');

View file

@ -0,0 +1,83 @@
<?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 ExternalAccountTest 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('ha1')
->assertSee('idkey')
->assertSee('depends_on');
}
}

View file

@ -21,7 +21,6 @@ namespace Tests;
use App\Password;
use App\Account;
use App\Helpers\Utils;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
@ -59,7 +58,7 @@ abstract class TestCase extends BaseTestCase
$challenge = \substr($response->headers->get('www-authenticate'), 7);
$extractedChallenge = $this->extractAuthenticateHeader($challenge);
$cnonce = Utils::generateNonce();
$cnonce = generateNonce();
$A1 = $password->password;
$A2 = hash($hash, $this->method . ':' . $this->route);

View file

@ -8,7 +8,7 @@
#%define _datadir %{_datarootdir}
#%define _docdir %{_datadir}/doc
%define build_number 142
%define build_number 143
%define var_dir /var/opt/belledonne-communications
%define opt_dir /opt/belledonne-communications/share/flexisip-account-manager