Fix #95 PUT /accounts admin endpoint implementation

This commit is contained in:
Timothée Jaussoin 2023-05-25 15:15:50 +00:00
parent ca4320e734
commit ec1bdba376
12 changed files with 164 additions and 153 deletions

View file

@ -32,6 +32,7 @@ use App\Password;
use App\EmailChanged;
use App\Mail\ChangingEmail;
use Carbon\Carbon;
use Illuminate\Http\Request;
class Account extends Authenticatable
{
@ -202,6 +203,19 @@ class Account extends Authenticatable
return null;
}
public function setPhoneAttribute(?string $phone)
{
$this->alias()->delete();
if (!empty($phone)) {
$alias = new Alias;
$alias->alias = $phone;
$alias->domain = config('app.sip_domain');
$alias->account_id = $this->id;
$alias->save();
}
}
public function getConfirmationKeyExpiresAttribute()
{
if ($this->activationExpiration) {
@ -302,9 +316,20 @@ class Account extends Authenticatable
return $this->provisioning_token;
}
public function isAdmin()
public function getAdminAttribute(): bool
{
return ($this->admin);
return ($this->admin()->exists());
}
public function setAdminAttribute(bool $isAdmin)
{
$this->admin()->delete();
if ($isAdmin) {
$admin = new Admin;
$admin->account_id = $this->id;
$admin->save();
}
}
public function hasTombstone()
@ -325,6 +350,14 @@ class Account extends Authenticatable
$password->save();
}
public function fillPassword(Request $request)
{
if ($request->filled('password')) {
$this->algorithm = $request->has('password_sha256') ? 'SHA-256' : 'MD5';
$this->updatePassword($request->get('password'), $this->algorithm);
}
}
public function toVcard4()
{
$vcard = 'BEGIN:VCARD

View file

@ -22,6 +22,7 @@ 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;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
@ -119,3 +120,13 @@ function isRegularExpression($string): bool
return $isRegularExpression;
}
function resolveDomain(Request $request): string
{
return $request->has('domain')
&& $request->user()
&& $request->user()->admin
&& config('app.admins_manage_multi_domains')
? $request->get('domain')
: config('app.sip_domain');
}

View file

@ -30,12 +30,6 @@ use App\Alias;
use App\ExternalAccount;
use App\Http\Requests\CreateAccountRequest;
use App\Http\Requests\UpdateAccountRequest;
use App\Rules\BlacklistedUsername;
use App\Rules\IsNotPhoneNumber;
use App\Rules\NoUppercase;
use App\Rules\SIPUsername;
use App\Rules\WithoutSpaces;
use Illuminate\Validation\Rule;
class AccountController extends Controller
{
@ -71,45 +65,19 @@ class AccountController extends Controller
public function store(CreateAccountRequest $request)
{
$request->validate([
'username' => [
'required',
new NoUppercase,
new IsNotPhoneNumber,
new BlacklistedUsername,
new SIPUsername,
Rule::unique('accounts', 'username')->where(function ($query) use ($request) {
$query->where('domain', $this->resolveDomain($request));
}),
'filled',
],
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
'email' => [
'nullable',
'email',
config('app.account_email_unique') ? Rule::unique('accounts', 'email') : null
],
'phone' => [
'nullable',
'unique:aliases,alias',
'unique:accounts,username',
new WithoutSpaces, 'starts_with:+'
]
]);
$account = new Account;
$account->username = $request->get('username');
$account->email = $request->get('email');
$account->display_name = $request->get('display_name');
$account->domain = $this->resolveDomain($request);
$account->domain = resolveDomain($request);
$account->ip_address = $request->ip();
$account->creation_time = Carbon::now();
$account->user_agent = config('app.name');
$account->dtmf_protocol = $request->get('dtmf_protocol');
$account->save();
$this->fillPassword($request, $account);
$this->fillPhone($request, $account);
$account->phone = $request->get('phone');
$account->fillPassword($request);
Log::channel('events')->info('Web Admin: Account created', ['id' => $account->identifier]);
@ -126,32 +94,6 @@ class AccountController extends Controller
public function update(UpdateAccountRequest $request, $id)
{
$request->validate([
'username' => [
'required',
new NoUppercase,
new IsNotPhoneNumber,
new BlacklistedUsername,
new SIPUsername,
Rule::unique('accounts', 'username')->where(function ($query) use ($request) {
$query->where('domain', $this->resolveDomain($request));
})->ignore($id),
'filled',
],
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
'email' => [
'nullable',
'email',
config('app.account_email_unique') ? Rule::unique('accounts', 'email')->ignore($id) : null
],
'phone' => [
'nullable',
'unique:aliases,alias',
'unique:accounts,username',
new WithoutSpaces, 'starts_with:+'
]
]);
$account = Account::findOrFail($id);
$account->username = $request->get('username');
$account->email = $request->get('email');
@ -159,8 +101,8 @@ class AccountController extends Controller
$account->dtmf_protocol = $request->get('dtmf_protocol');
$account->save();
$this->fillPassword($request, $account);
$this->fillPhone($request, $account);
$account->phone = $request->get('phone');
$account->fillPassword($request);
Log::channel('events')->info('Web Admin: Account updated', ['id' => $account->identifier]);
@ -262,25 +204,4 @@ class AccountController extends Controller
return redirect()->route('admin.account.index');
}
private function fillPassword(Request $request, Account $account)
{
if ($request->filled('password')) {
$algorithm = $request->has('password_sha256') ? 'SHA-256' : 'MD5';
$account->updatePassword($request->get('password'), $algorithm);
}
}
private function fillPhone(Request $request, Account $account)
{
if ($request->filled('phone')) {
$account->alias()->delete();
$alias = new Alias;
$alias->alias = $request->get('phone');
$alias->domain = config('app.sip_domain');
$alias->account_id = $account->id;
$alias->save();
}
}
}

View file

@ -22,7 +22,6 @@ namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
@ -33,12 +32,9 @@ use App\ActivationExpiration;
use App\Admin;
use App\Alias;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Http\Requests\CreateAccountRequest;
use App\Http\Requests\UpdateAccountRequest;
use App\Mail\PasswordAuthentication;
use App\Rules\BlacklistedUsername;
use App\Rules\IsNotPhoneNumber;
use App\Rules\NoUppercase;
use App\Rules\SIPUsername;
use App\Rules\WithoutSpaces;
use Illuminate\Support\Facades\Mail;
class AccountController extends Controller
@ -112,36 +108,15 @@ class AccountController extends Controller
return $account->makeVisible(['provisioning_token']);
}
public function store(Request $request)
public function store(CreateAccountRequest $request)
{
$request->validate([
'username' => [
'required',
new NoUppercase,
new IsNotPhoneNumber,
new BlacklistedUsername,
new SIPUsername,
Rule::unique('accounts', 'username')->where(function ($query) use ($request) {
$query->where('domain', $this->resolveDomain($request));
}),
'filled',
],
'algorithm' => 'required|in:SHA-256,MD5',
'password' => 'required|filled',
'admin' => 'boolean|nullable',
'activated' => 'boolean|nullable',
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
'confirmation_key_expires' => [
'date_format:Y-m-d H:i:s',
'nullable',
],
'email' => config('app.account_email_unique')
? 'nullable|email|unique:accounts,email'
: 'nullable|email',
'phone' => [
'unique:aliases,alias',
'unique:accounts,username',
new WithoutSpaces, 'starts_with:+'
]
]);
@ -153,7 +128,7 @@ class AccountController extends Controller
$account->ip_address = $request->ip();
$account->dtmf_protocol = $request->get('dtmf_protocol');
$account->creation_time = Carbon::now();
$account->domain = $this->resolveDomain($request);
$account->domain = resolveDomain($request);
$account->user_agent = $request->header('User-Agent') ?? config('app.name');
if (!$request->has('activated') || !(bool)$request->get('activated')) {
@ -172,27 +147,45 @@ class AccountController extends Controller
}
$account->updatePassword($request->get('password'), $request->get('algorithm'));
if ($request->has('admin') && (bool)$request->get('admin')) {
$admin = new Admin;
$admin->account_id = $account->id;
$admin->save();
}
if ($request->has('phone')) {
$alias = new Alias;
$alias->alias = $request->get('phone');
$alias->domain = config('app.sip_domain');
$alias->account_id = $account->id;
$alias->save();
}
$account->admin = $request->has('admin') && (bool)$request->get('admin');
$account->phone = $request->get('phone');
// Full reload
$account = Account::withoutGlobalScopes()->find($account->id);
Log::channel('events')->info('API Admin: Account created', ['id' => $account->identifier]);
return response()->json($account->makeVisible(['confirmation_key', 'provisioning_token']));
return $account->makeVisible(['confirmation_key', 'provisioning_token']);
}
public function update(UpdateAccountRequest $request, int $accountId)
{
$request->validate([
'algorithm' => 'required|in:SHA-256,MD5',
'admin' => 'boolean|nullable',
'activated' => 'boolean|nullable'
]);
$account = Account::findOrFail($accountId);
$account->username = $request->get('username');
$account->email = $request->get('email');
$account->display_name = $request->get('display_name');
$account->dtmf_protocol = $request->get('dtmf_protocol');
$account->domain = resolveDomain($request);
$account->user_agent = $request->header('User-Agent') ?? config('app.name');
$account->save();
$account->updatePassword($request->get('password'), $request->get('algorithm'));
$account->admin = $request->has('admin') && (bool)$request->get('admin');
$account->phone = $request->get('phone');
// Full reload
$account = Account::withoutGlobalScopes()->find($account->id);
Log::channel('events')->info('API Admin: Account updated', ['id' => $account->identifier]);
return $account->makeVisible(['confirmation_key', 'provisioning_token']);
}
public function typeAdd(int $id, int $typeId)
@ -226,6 +219,6 @@ class AccountController extends Controller
Mail::to($account)->send(new PasswordAuthentication($account));
return response()->json($account->makeVisible(['confirmation_key', 'provisioning_token']));
return $account->makeVisible(['confirmation_key', 'provisioning_token']);
}
}

View file

@ -11,14 +11,4 @@ use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
protected function resolveDomain(Request $request): string
{
return $request->has('domain')
&& $request->user()
&& $request->user()->isAdmin()
&& config('app.admins_manage_multi_domains')
? $request->get('domain')
: config('app.sip_domain');
}
}

View file

@ -19,7 +19,7 @@ class AuthenticateAdmin
return redirect()->route('account.login');
}
if (!$request->user()->isAdmin()) {
if (!$request->user()->admin) {
return abort(403, 'Unauthorized area');
}

View file

@ -29,13 +29,14 @@ class CreateAccountRequest extends FormRequest
new BlacklistedUsername,
new SIPUsername,
Rule::unique('accounts', 'username')->where(function ($query) {
$query->where('domain', config('app.sip_domain'));
$query->where('domain', resolveDomain($this));
}),
'filled',
],
'domain' => config('app.admins_manage_multi_domains') ? 'required' : '',
'password' => 'required|min:3',
'email' => 'nullable|email',
'email' => config('app.account_email_unique')
? 'nullable|email|unique:accounts,email'
: 'nullable|email',
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
'phone' => [
'nullable',

View file

@ -33,8 +33,11 @@ class UpdateAccountRequest extends FormRequest
})->ignore($this->route('id'), 'id'),
'filled',
],
'domain' => config('app.admins_manage_multi_domains') ? 'required' : '',
'email' => 'nullable|email',
'email' => [
'nullable',
'email',
config('app.account_email_unique') ? Rule::unique('accounts', 'email')->ignore($this->route('id')) : null
],
'password_sha256' => 'nullable|min:3',
'dtmf_protocol' => 'nullable|in:' . Account::dtmfProtocolsRule(),
'phone' => [

View file

@ -45,7 +45,7 @@
</a>
</div>
@if($account->isAdmin())
@if($account->admin)
<h3>Admin area</h3>
<div class="list-group mb-3">
<a href="{{ route('admin.account.index') }}" class="list-group-item list-group-item-action">

View file

@ -291,6 +291,21 @@ JSON parameters:
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
* `confirmation_key_expires` optional, a datetime of this format: Y-m-d H:i:s. Only used when `activated` is not used or `false`. Enforces an expiration date on the returned `confirmation_key`. After that datetime public email or phone activation endpoints will return `403`.
### `PUT /accounts/{id}`
<span class="badge badge-warning">Admin</span>
Update an existing account.
JSON parameters:
* `username` unique username, minimum 6 characters
* `password` required minimum 6 characters
* `algorithm` required, values can be `SHA-256` or `MD5`
* `display_name` optional, string
* `email` optional, must be an email, must be unique if `ACCOUNT_EMAIL_UNIQUE` is set to `true`
* `admin` optional, a boolean, set to `false` by default, create an admin account
* `phone` optional, a phone number, set a phone number to the account
* `dtmf_protocol` optional, values must be `sipinfo`, `sipmessage` or `rfc2833`
### `GET /accounts`
<span class="badge badge-warning">Admin</span>
Retrieve all the accounts, paginated.

View file

@ -88,11 +88,12 @@ Route::group(['middleware' => ['auth.digest_or_key']], function () {
Route::post('accounts/{id}/recover-by-email', 'Api\Admin\AccountController@recoverByEmail');
Route::post('accounts', 'Api\Admin\AccountController@store');
Route::put('accounts/{id}', 'Api\Admin\AccountController@update');
Route::get('accounts', 'Api\Admin\AccountController@index');
Route::get('accounts/{sip}/search', 'Api\Admin\AccountController@search');
Route::get('accounts/{email}/search-by-email', 'Api\Admin\AccountController@searchByEmail');
Route::get('accounts/{id}', 'Api\Admin\AccountController@show');
Route::delete('accounts/{id}', 'Api\Admin\AccountController@destroy');
Route::get('accounts/{sip}/search', 'Api\Admin\AccountController@search');
Route::get('accounts/{email}/search-by-email', 'Api\Admin\AccountController@searchByEmail');
// Account actions
Route::get('accounts/{id}/actions', 'Api\Admin\AccountActionController@index');

View file

@ -319,7 +319,7 @@ class ApiAccountTest extends TestCase
->json($this->method, $this->route, [
'username' => $username,
'algorithm' => 'SHA-256',
'password' => '2',
'password' => 'blabla',
'admin' => true,
]);
@ -349,7 +349,7 @@ class ApiAccountTest extends TestCase
->json($this->method, $this->route, [
'username' => $username,
'algorithm' => 'SHA-256',
'password' => '2',
'password' => 'blabla',
'activated' => true,
]);
@ -379,7 +379,7 @@ class ApiAccountTest extends TestCase
->json($this->method, $this->route, [
'username' => $username,
'algorithm' => 'SHA-256',
'password' => '2',
'password' => 'blabla',
'activated' => false,
]);
@ -533,6 +533,49 @@ class ApiAccountTest extends TestCase
->assertJsonValidationErrors(['email']);
}
public function testEditAdmin()
{
$password = Password::factory()->create();
$account = $password->account;
$admin = Admin::factory()->create();
$admin->account->generateApiKey();
$admin->account->save();
$username = 'changed';
$algorithm = 'MD5';
$password = 'other';
$this->keyAuthenticated($admin->account)
->json('PUT', $this->route. '/1234')
->assertStatus(422)
->assertJsonValidationErrors(['username']);
$this->keyAuthenticated($admin->account)
->json('PUT', $this->route. '/1234', [
'username' => 'good'
])
->assertStatus(422);
$this->keyAuthenticated($admin->account)
->json('PUT', $this->route. '/'. $account->id, [
'username' => $username,
'algorithm' => $algorithm,
'password' => $password,
])
->assertStatus(200);
$this->assertDatabaseHas('accounts', [
'id' => $account->id,
'username' => $username
]);
$this->assertDatabaseHas('passwords', [
'account_id' => $account->id,
'algorithm' => $algorithm
]);
}
/**
* /!\ Dangerous endpoints
*/