Add account expirations table

Complete POST /accounts admin endpoints
Handle expiration in email and phone endpoints
Complete documentation
Add related tests
Bump package version
This commit is contained in:
Timothée Jaussoin 2021-02-23 17:45:33 +01:00
parent cd32657d21
commit 46af75fea3
9 changed files with 176 additions and 13 deletions

View file

@ -38,10 +38,10 @@ class Account extends Authenticatable
use HasFactory;
protected $connection = 'external';
protected $with = ['passwords', 'admin', 'emailChanged', 'alias'];
protected $with = ['passwords', 'admin', 'emailChanged', 'alias', 'activationExpiration'];
protected $hidden = ['alias', 'expire_time', 'confirmation_key'];
protected $dateTimes = ['creation_time'];
protected $appends = ['realm', 'phone'];
protected $appends = ['realm', 'phone', 'confirmation_key_expires'];
protected $casts = [
'activated' => 'boolean',
];
@ -97,6 +97,11 @@ class Account extends Authenticatable
return $this->hasOne('App\Admin');
}
public function activationExpiration()
{
return $this->hasOne('App\ActivationExpiration');
}
public function apiKey()
{
return $this->hasOne('App\ApiKey');
@ -131,6 +136,20 @@ class Account extends Authenticatable
return null;
}
public function getConfirmationKeyExpiresAttribute()
{
if ($this->activationExpiration) {
return $this->activationExpiration->expires->format('Y-m-d H:i:s');
}
return null;
}
public function activationExpired(): bool
{
return ($this->activationExpiration && $this->activationExpiration->isExpired());
}
public function requestEmailUpdate(string $newEmail)
{
// Remove all the old requests

View file

@ -0,0 +1,28 @@
<?php
namespace App;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ActivationExpiration extends Model
{
use HasFactory;
protected $connection = 'local';
protected $casts = [
'expires' => 'datetime:Y-m-d H:i:s',
];
public function account()
{
return $this->belongsTo('App\Account');
}
public function isExpired()
{
$now = Carbon::now();
return $this->expires->lessThan($now);
}
}

View file

@ -16,6 +16,7 @@ class AccountDeleting
{
$account->alias()->delete();
$account->passwords()->delete();
$account->activationExpiration()->delete();
$account->nonces()->delete();
$account->admin()->delete();
$account->apiKey()->delete();

View file

@ -100,9 +100,11 @@ class AccountController extends Controller
$account = Account::sip($sip)
->where('confirmation_key', $request->get('code'))
->firstOrFail();
if ($account->activationExpired()) abort(403, 'Activation expired');
$account->activated = true;
$account->confirmation_key = null;
$account->save();
return $account;
@ -117,6 +119,9 @@ class AccountController extends Controller
$account = Account::sip($sip)
->where('confirmation_key', $request->get('code'))
->firstOrFail();
if ($account->activationExpired()) abort(403, 'Activation expired');
$account->activated = true;
$account->confirmation_key = null;
$account->save();

View file

@ -26,10 +26,9 @@ use Illuminate\Validation\Rule;
use Carbon\Carbon;
use App\Account;
use App\ActivationExpiration;
use App\Admin;
use App\Password;
use App\Rules\WithoutSpaces;
use App\Helpers\Utils;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
class AccountController extends Controller
@ -84,6 +83,10 @@ class AccountController extends Controller
'domain' => 'min:3',
'admin' => 'boolean|nullable',
'activated' => 'boolean|nullable',
'confirmation_key_expires' => [
'date_format:Y-m-d H:i:s',
'nullable',
]
]);
$account = new Account;
@ -105,6 +108,14 @@ class AccountController extends Controller
$account->save();
if ((!$request->has('activated') || !(bool)$request->get('activated'))
&& $request->has('confirmation_key_expires')) {
$actionvationExpiration = new ActivationExpiration;
$actionvationExpiration->account_id = $account->id;
$actionvationExpiration->expires = $request->get('confirmation_key_expires');
$actionvationExpiration->save();
}
$account->updatePassword($request->get('password'), $request->get('algorithm'));
if ($request->has('admin') && (bool)$request->get('admin')) {

View file

@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateActivationExpirationsTable extends Migration
{
public function up()
{
Schema::connection('local')->create('activation_expirations', function (Blueprint $table) {
$table->id();
$table->integer('account_id')->unsigned();
$table->dateTime('expires');
$table->timestamps();
});
}
public function down()
{
Schema::connection('local')->dropIfExists('activation_expirations');
}
}

View file

@ -157,7 +157,7 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth
<h4><code>POST /accounts</code></h4>
<p>To create an account directly from the API.</p>
<p>If <code>activated</code> is set to <code>false</code> a random generated <code>confirmation_key</code> will be returned to allow further activation using the public endpoints.</p>
<p>If <code>activated</code> is set to <code>false</code> a random generated <code>confirmation_key</code> will be returned to allow further activation using the public endpoints. Check <code>confirmation_key_expires</code> to also set an expiration date on that <code>confirmation_key</code>.</p>
<p>JSON parameters:</p>
<ul>
@ -167,6 +167,7 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth
<li><code>domain</code> optional, the value is set to the default registration domain if not set</li>
<li><code>activated</code> optional, a boolean, set to <code>false</code> by default</li>
<li><code>admin</code> optional, a boolean, set to <code>false</code> by default, create an admin account</li>
<li><code>confirmation_key_expires</code> optional, a datetime of this format: Y-m-d H:i:s. Only used when <code>activated</code> is not used or <code>false</code>. Enforces an expiration date on the returned <code>confirmation_key</code>. After that datetime public email or phone activation endpoints will return <code>403</code>.</li>
</ul>
<h4><code>GET /accounts</code></h4>

View file

@ -21,8 +21,9 @@ namespace Tests\Feature;
use App\Password;
use App\Account;
use App\ActivationExpiration;
use App\Admin;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
@ -226,8 +227,7 @@ class AccountApiTest extends TestCase
'activated' => false,
]);
$response1
->assertStatus(200)
$response1->assertStatus(200)
->assertJson([
'id' => 2,
'username' => $username,
@ -296,6 +296,11 @@ class AccountApiTest extends TestCase
$password->account->activated = false;
$password->account->save();
$expiration = new ActivationExpiration;
$expiration->account_id = $password->account->id;
$expiration->expires = Carbon::now()->subYear();
$expiration->save();
$this->get($this->route.'/'.$password->account->identifier.'/info')
->assertStatus(200)
->assertJson([
@ -308,20 +313,31 @@ class AccountApiTest extends TestCase
])
->assertStatus(404);
$activateEmailRoute = $this->route.'/'.$password->account->identifier.'/activate/email';
$this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/email', [
->json($this->method, $activateEmailRoute, [
'code' => $confirmationKey.'longer'
])
->assertStatus(422);
$this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/email', [
->json($this->method, $activateEmailRoute, [
'code' => 'X123456789abc'
])
->assertStatus(404);
// Expired
$this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/email', [
->json($this->method, $activateEmailRoute, [
'code' => $confirmationKey
])
->assertStatus(403);
$expiration->delete();
$this->keyAuthenticated($password->account)
->json($this->method, $activateEmailRoute, [
'code' => $confirmationKey
])
->assertStatus(200);
@ -342,12 +358,26 @@ class AccountApiTest extends TestCase
$password->account->activated = false;
$password->account->save();
$expiration = new ActivationExpiration;
$expiration->account_id = $password->account->id;
$expiration->expires = Carbon::now()->subYear();
$expiration->save();
$this->get($this->route.'/'.$password->account->identifier.'/info')
->assertStatus(200)
->assertJson([
'activated' => false
]);
// Expired
$this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/phone', [
'code' => $confirmationKey
])
->assertStatus(403);
$expiration->delete();
$this->keyAuthenticated($password->account)
->json($this->method, $this->route.'/'.$password->account->identifier.'/activate/phone', [
'code' => $confirmationKey
@ -544,6 +574,51 @@ class AccountApiTest extends TestCase
]);
}
public function testCodeExpires()
{
$admin = Admin::factory()->create();
$admin->account->generateApiKey();
// Activated, no no confirmation_key
$this->keyAuthenticated($admin->account)
->json($this->method, $this->route, [
'username' => 'foobar',
'algorithm' => 'SHA-256',
'password' => '123456',
'activated' => true,
'confirmation_key_expires' => '2040-12-12 12:12:12'
])
->assertStatus(200)
->assertJson([
'confirmation_key_expires' => null
]);
// Bad datetime format
$this->keyAuthenticated($admin->account)
->json($this->method, $this->route, [
'username' => 'foobar2',
'algorithm' => 'SHA-256',
'password' => '123456',
'activated' => false,
'confirmation_key_expires' => 'abc'
])
->assertStatus(422);
// Bad datetime format
$this->keyAuthenticated($admin->account)
->json($this->method, $this->route, [
'username' => 'foobar2',
'algorithm' => 'SHA-256',
'password' => '123456',
'activated' => false,
'confirmation_key_expires' => '2040-12-12 12:12:12'
])
->assertStatus(200)
->assertJson([
'confirmation_key_expires' => '2040-12-12 12:12:12'
]);;
}
public function testDelete()
{
$password = Password::factory()->create();

View file

@ -8,7 +8,7 @@
#%define _datadir %{_datarootdir}
#%define _docdir %{_datadir}/doc
%define build_number 52
%define build_number 53
%define var_dir /var/opt/belledonne-communications
%define opt_dir /opt/belledonne-communications/share/flexisip-account-manager
%define env_file "$RPM_BUILD_ROOT/etc/flexisip-account-manager/flexiapi.env"