Add a permanent provisioning URL, authenticated

Complete the documentation
Add a few tests for the provisioning urls
Update the dependencies
Bump the package version
This commit is contained in:
Timothée Jaussoin 2021-08-04 16:48:25 +02:00
parent 877cae94f7
commit b48c8f505d
7 changed files with 172 additions and 54 deletions

View file

@ -51,7 +51,15 @@ class ProvisioningController extends Controller
return response($result->getString())->header('Content-Type', $result->getMimeType());
}
public function show(Request $request, $confirmationKey = null)
/**
* Authenticated provisioning
*/
public function me(Request $request)
{
return $this->show($request, null, $request->user());
}
public function show(Request $request, $confirmationKey = null, Account $requestAccount = null)
{
// Load the hooks if they exists
$provisioningHooks = config_path('provisioning_hooks.php');
@ -99,67 +107,71 @@ class ProvisioningController extends Controller
$account = null;
// Account handling
if ($confirmationKey) {
if ($requestAccount) {
$account = $requestAccount;
} else if ($confirmationKey) {
$account = Account::withoutGlobalScopes()
->where('confirmation_key', $confirmationKey)
->first();
}
if ($account && !$account->activationExpired()) {
if ($account && !$account->activationExpired()) {
$section = $dom->createElement('section');
$section->setAttribute('name', 'proxy_' . $proxyConfigIndex);
$entry = $dom->createElement('entry', $account->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);
// Complete the section with the Proxy hook
if (function_exists('provisioningProxyHook')) {
provisioningProxyHook($section, $request, $account);
}
$config->appendChild($section);
$passwords = $account->passwords()->get();
foreach ($passwords as $password) { // => foreach ($passwords)
$section = $dom->createElement('section');
$section->setAttribute('name', 'proxy_' . $proxyConfigIndex);
$section->setAttribute('name', 'auth_info_' . $authInfoIndex);
$entry = $dom->createElement('entry', $account->identifier);
$entry->setAttribute('name', 'reg_identity');
$entry->setAttribute('name', 'username');
$section->appendChild($entry);
$entry = $dom->createElement('entry', 1);
$entry->setAttribute('name', 'reg_sendregister');
$entry = $dom->createElement('entry', $password->password);
$entry->setAttribute('name', 'ha1');
$section->appendChild($entry);
$entry = $dom->createElement('entry', 'push_notification');
$entry->setAttribute('name', 'refkey');
$entry = $dom->createElement('entry', $account->resolvedRealm);
$entry->setAttribute('name', 'realm');
$section->appendChild($entry);
// Complete the section with the Proxy hook
if (function_exists('provisioningProxyHook')) {
provisioningProxyHook($section, $request, $account);
$entry = $dom->createElement('entry', $password->algorithm);
$entry->setAttribute('name', 'algorithm');
$section->appendChild($entry);
// Complete the section with the Auth hook
if (function_exists('provisioningAuthHook')) {
provisioningAuthHook($section, $request, $password);
}
$config->appendChild($section);
$passwords = $account->passwords()->get();
$authInfoIndex++;
foreach ($passwords as $password) { // => foreach ($passwords)
$section = $dom->createElement('section');
$section->setAttribute('name', 'auth_info_' . $authInfoIndex);
$entry = $dom->createElement('entry', $account->identifier);
$entry->setAttribute('name', 'username');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $password->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', $password->algorithm);
$entry->setAttribute('name', 'algorithm');
$section->appendChild($entry);
// Complete the section with the Auth hook
if (function_exists('provisioningAuthHook')) {
provisioningAuthHook($section, $request, $password);
}
$config->appendChild($section);
$authInfoIndex++;
}
}
if ($confirmationKey) {
$account->confirmation_key = null;
$account->save();
}

15
flexiapi/composer.lock generated
View file

@ -979,16 +979,16 @@
},
{
"name": "laravel/framework",
"version": "v8.52.0",
"version": "v8.53.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "8fe9877d52e25f8aed36c51734e5a8510be967e6"
"reference": "4b2e3e7317da82dd9f5b88d477abd93444748b43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/8fe9877d52e25f8aed36c51734e5a8510be967e6",
"reference": "8fe9877d52e25f8aed36c51734e5a8510be967e6",
"url": "https://api.github.com/repos/laravel/framework/zipball/4b2e3e7317da82dd9f5b88d477abd93444748b43",
"reference": "4b2e3e7317da82dd9f5b88d477abd93444748b43",
"shasum": ""
},
"require": {
@ -1061,7 +1061,7 @@
"illuminate/view": "self.version"
},
"require-dev": {
"aws/aws-sdk-php": "^3.155",
"aws/aws-sdk-php": "^3.186.4",
"doctrine/dbal": "^2.6|^3.0",
"filp/whoops": "^2.8",
"guzzlehttp/guzzle": "^6.5.5|^7.0.1",
@ -1074,7 +1074,7 @@
"symfony/cache": "^5.1.4"
},
"suggest": {
"aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.155).",
"aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.186.4).",
"brianium/paratest": "Required to run tests in parallel (^6.0).",
"doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6|^3.0).",
"ext-ftp": "Required to use the Flysystem FTP driver.",
@ -1143,7 +1143,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2021-07-27T13:03:29+00:00"
"time": "2021-08-03T14:36:33+00:00"
},
{
"name": "laravel/tinker",
@ -7493,7 +7493,6 @@
"type": "github"
}
],
"abandoned": true,
"time": "2020-09-28T06:45:17+00:00"
},
{

View file

@ -20,6 +20,8 @@
namespace Database\Factories;
use App\Account;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;
class AccountFactory extends Factory
@ -33,6 +35,7 @@ class AccountFactory extends Factory
'domain' => config('app.sip_domain'),
'email' => $this->faker->email,
'user_agent' => $this->faker->userAgent,
'confirmation_key' => Str::random(WebAuthenticateController::$emailCodeSize),
'ip_address' => $this->faker->ipv4,
'creation_time' => $this->faker->dateTime,
'activated' => true

View file

@ -192,9 +192,17 @@ When an account is having an available `confirmation_key` it can be provisioned
Those two URL are <b>not API endpoints</b>, they are not located under `/api`.
### `VISIT /provisioning/{confirmation_key}`
### `VISIT /provisioning/`
Return the provisioning information available in the liblinphone configuration file (if correctly configured).
If the `confirmation_key` is valid the related account information are added to the returned XML. The account is then considered as "provisioned" and those account related information will be removed in the upcoming requests.
### `VISIT /provisioning/{confirmation_key}`
Return the provisioning information available in the liblinphone configuration file.
If the `confirmation_key` is valid the related account information are added to the returned XML. The account is then considered as "provisioned" and those account related information will be removed in the upcoming requests (the content will be the same as the previous url).
### `VISIT /provisioning/qrcode/{confirmation_key}`
Return a QRCode that points to the provisioning URL.
## Authenticated provisioning
### `VISIT /provisioning/me`
Return the same base content as the previous URL and the account related information, similar to the `confirmation_key` endpoint. However this endpoint will always return those information.

View file

@ -32,6 +32,10 @@ Route::get('login/phone', 'Account\AuthenticateController@loginPhone')->name('ac
Route::post('authenticate/phone', 'Account\AuthenticateController@authenticatePhone')->name('account.authenticate.phone');
Route::post('authenticate/phone/confirm', 'Account\AuthenticateController@validatePhone')->name('account.authenticate.phone_confirm');
Route::group(['middleware' => 'auth.digest_or_key'], function () {
Route::get('provisioning/me', 'Account\ProvisioningController@me')->name('provisioning.me');
});
Route::get('provisioning/qrcode/{confirmation}', 'Account\ProvisioningController@qrcode')->name('provisioning.qrcode');
Route::get('provisioning/{confirmation?}', 'Account\ProvisioningController@show')->name('provisioning.show');

View file

@ -0,0 +1,92 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2021 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 Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use App\Password;
class AccountProvisioningTest extends TestCase
{
use RefreshDatabase;
protected $route = '/provisioning';
protected $accountRoute = '/provisioning/me';
protected $method = 'GET';
protected $pnProvider = 'provider';
protected $pnParam = 'param';
protected $pnPrid = 'id';
public function testBaseProvisioning()
{
$response = $this->get($this->route);
$response->assertStatus(200);
$response->assertHeader('Content-Type', 'application/xml');
$response->assertDontSee('ha1');
}
public function testAuthenticatedProvisioning()
{
$response = $this->get($this->accountRoute);
$response->assertStatus(302);
$password = Password::factory()->create();
$password->account->generateApiKey();
// Ensure that we get the authentication password once
$response = $this->keyAuthenticated($password->account)
->get($this->accountRoute)
->assertStatus(200)
->assertHeader('Content-Type', 'application/xml')
->assertSee('ha1');
// And then twice
$response = $this->keyAuthenticated($password->account)
->get($this->accountRoute)
->assertStatus(200)
->assertHeader('Content-Type', 'application/xml')
->assertSee('ha1');
}
public function testConfirmationKeyProvisioning()
{
$response = $this->get($this->route.'/1234');
$response->assertStatus(200);
$response->assertHeader('Content-Type', 'application/xml');
$response->assertDontSee('ha1');
$password = Password::factory()->create();
$password->account->generateApiKey();
// Ensure that we get the authentication password once
$response = $this->get($this->route.'/'.$password->account->confirmation_key)
->assertStatus(200)
->assertHeader('Content-Type', 'application/xml')
->assertSee('ha1');
// And then twice
$response = $this->get($this->route.'/'.$password->account->confirmation_key)
->assertStatus(200)
->assertHeader('Content-Type', 'application/xml')
->assertDontSee('ha1');
}
}

View file

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