Compare commits

...

11 commits

31 changed files with 855 additions and 653 deletions

View file

@ -14,13 +14,13 @@ rocky9-deploy:
- rocky9-package
- rocky9-test
debian11-deploy:
extends: .deploy
script:
- ./deploy_packages.sh debian bullseye
needs:
- debian11-package
- debian11-test
#debian11-deploy:
# extends: .deploy
# script:
# - ./deploy_packages.sh debian bullseye
# needs:
# - debian11-package
# - debian11-test
debian12-deploy:
extends: .deploy

View file

@ -16,9 +16,9 @@ rocky9-package:
script:
- make rpm-el9
debian11-package:
extends: .debian_package
image: gitlab.linphone.org:4567/bc/public/docker/debian11-php:$DEBIAN_11_IMAGE_VERSION
#debian11-package:
# extends: .debian_package
# image: gitlab.linphone.org:4567/bc/public/docker/debian11-php:$DEBIAN_11_IMAGE_VERSION
debian12-package:
extends: .debian_package

View file

@ -21,11 +21,11 @@ rocky9-test:
- php artisan key:generate
- vendor/bin/phpunit --log-junit $CI_PROJECT_DIR/flexiapi_phpunit.log
debian11-test:
extends: .debian-test
image: gitlab.linphone.org:4567/bc/public/docker/debian11-php:$DEBIAN_11_IMAGE_VERSION
needs:
- debian11-package
#debian11-test:
# extends: .debian-test
# image: gitlab.linphone.org:4567/bc/public/docker/debian11-php:$DEBIAN_11_IMAGE_VERSION
# needs:
# - debian11-package
debian12-test:
extends: .debian-test

View file

@ -1,8 +1,8 @@
variables:
ROCKY_8_IMAGE_VERSION: 20241112_111033_update_package_and_dependencies
ROCKY_9_IMAGE_VERSION: 20241112_115442_add_php_sodium
DEBIAN_11_IMAGE_VERSION: 20241112_113527_update_package_and_dependencies
DEBIAN_12_IMAGE_VERSION: 20241112_113948_update_package_and_dependencies
# DEBIAN_11_IMAGE_VERSION: 20241204_161845_update_download_linphone_org
DEBIAN_12_IMAGE_VERSION: 20241204_162237_update_download_linphone_org
PHP_REDIS_REMI_VERSION: php-pecl-redis5-5.3.6-1
PHP_IGBINARY_REMI_VERSION: php-pecl-igbinary-3.2.14-1
PHP_MSGPACK_REMI_VERSION: php-pecl-msgpack-2.2.0-1

View file

@ -1,5 +1,33 @@
# Flexisip Account Manager Changelog
v1.6.7
---
- Fix FLEXIAPI-367 Bump laravel-redis-sentinel dependency to support Redis 6
v1.6.6
---
- Fix FLEXIAPI-365 Use mustache syntaxe for HCaptcha, small CSS fix and update the dependencies
v1.6.5
---
- Fix FLEXIAPI-269 Update the IsNotPhoneNumber rule to use a better phone number validator
v1.6.4
---
- Fix FLEXIAPI-268 Allow pn-param in Apple format for the push notifications endpoints
v1.6.3
---
- Fix FLEXIAPI-264 Add -k|api_key_ip parameter to accounts:create-admin-account...
v1.6.2
---
- Fix FLEXIAPI-262 Bypass the JWT auth if we have an API
v1.6.1
---
- Fix FLEXIAPI-254 Allow no data on POST requests to not trigger the ValidateJSON middleware
v1.6
----
- Fix FLEXIAPI-192 Add DotEnv configuration to allow the expiration of tokens and codes in the app
@ -13,6 +41,14 @@ v1.6
- Fix FLEXIAPI-239 Ensure to return the correct error codes as stated in the RFC6750 section 3.1
- Fix FLEXIAPI-238 Replace Material Icons with Phosphor
- Fix FLEXIAPI-240 Update the Docker images
- Fix GH-15 Add password import from CSV
- Fix FLEXIAPI-242 Add stricter validation for the AccountCreationToken Push Notification endpoint
- Fix FLEXIAPI-252 Update the hCaptcha Laravel library, use file instead of cookies to store the session to prevent empty errors bags
- Fix FLEXIAPI-254 Allow no data on POST requests to not trigger the ValidateJSON middleware
- Fix FLEXIAPI-262 Bypass the JWT auth if we have an API Key
- Fix FLEXIAPI-264 Add -k|api_key_ip parameter to accounts:create-admin-account to set/clear the related API Key restriction
- Fix FLEXIAPI-268 Allow pn-param in Apple format for the push notifications endpoints
- Fix FLEXIAPI-269 Update the IsNotPhoneNumber rule to use a better phone number validator
v1.5
---

View file

@ -156,11 +156,11 @@ Create or update a SIP Domain, required to then create accounts afterward. The `
### Create an admin account
Create an admin account, an API Key will also be generated along the way, it might expire after a while.
Create an admin account, an API Key will also be generated along the way, it might expire after a while (regarding the API Key expiration policy). An empty `api_key_ip` will remove the IP restriction on the key.
If no parameters are put, a default admin account will be created.
php artisan accounts:create-admin-account {-u|username=} {-p|password=} {-d|domain=}
php artisan accounts:create-admin-account {-u|username=} {-p|password=} {-d|domain=} {-k|api_key_ip=}
### Clear the expired API Keys

View file

@ -1,10 +1,28 @@
# Releases
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/).
## [1.6] - 2024-11-12
## Added
- **Allow the expiration of tokens and codes in the DotEnv configuration**
- **New DotEnv variables:** check all the new `*_EXPIRATION_MINUTES` for each token and code in `.env.example`
- **Phone validation system by country code:** all the provided phone numbers are now properly validated and some countries can be forbidden
- **SIP Domain management:** the account domains are now managed in a set of panels and API endpoints, this is the base of the upcoming space administration system
- **JSON validation in the API:** the provised JSON is now validated and returns an error if an issue is detected
- **CoTURN credentials support:** TURN credentials can now be generated and return through the provisioning feature
## Changed
- **Replace Material Icons with Phosphor**
## Deprecated
- **Last major version supporting the deprecated endpoints of the API**
## [1.5] - 2024-08-29
### Added

View file

@ -89,7 +89,7 @@ REDIS_DB=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=cookie
SESSION_DRIVER=file
SESSION_LIFETIME=120
# SMTP and emails

View file

@ -347,7 +347,7 @@ class Account extends Authenticatable
return ($this->activationExpiration && $this->activationExpiration->isExpired());
}
public function generateApiKey(?Request $request = null): ApiKey
public function generateApiKey(?string $ip = null): ApiKey
{
$this->apiKey()->delete();
@ -355,7 +355,7 @@ class Account extends Authenticatable
$apiKey->account_id = $this->id;
$apiKey->last_used_at = Carbon::now();
$apiKey->key = Str::random(40);
$apiKey->ip = $request ? $request->ip() : '127.0.0.1';
$apiKey->ip = $ip;
$apiKey->save();
return $apiKey;

View file

@ -19,6 +19,7 @@
namespace App;
use Illuminate\Validation\Rule;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class AccountCreationToken extends Consommable

View file

@ -27,8 +27,8 @@ use App\SipDomain;
class CreateAdminAccount extends Command
{
protected $signature = 'accounts:create-admin-account {--u|username=} {--p|password=} {--d|domain=}';
protected $description = 'Create an admin account';
protected $signature = 'accounts:create-admin-account {--u|username=} {--p|password=} {--d|domain=} {--k|api_key_ip=}';
protected $description = 'Create an admin account and generate an API Key';
public function __construct()
{
@ -90,10 +90,10 @@ class CreateAdminAccount extends Command
$account->created_at = Carbon::now()->subYears(3);
$account->save();
$account->generateApiKey();
$account->generateApiKey(ip: $this->option('api_key_ip') ?? null);
$account->updatePassword($password);
$this->info('Admin test account created: "' . $username . '@' . $domain . '" | Password: "' . $password . '" | API Key: "' . $account->apiKey->key . '"');
$this->info('Admin test account created: "' . $username . '@' . $domain . '" | Password: "' . $password . '" | API Key: "' . $account->apiKey->key . '" (valid on ' . ($account->apiKey->ip ?? 'any') . ' ip)');
return 0;
}

View file

@ -34,7 +34,7 @@ class ApiKeyController extends Controller
public function update(Request $request)
{
$account = $request->user();
$account->generateApiKey($request);
$account->generateApiKey($request->ip());
return redirect()->back();
}

View file

@ -20,6 +20,7 @@
namespace App\Http\Controllers\Admin;
use App\Account;
use App\Password;
use App\Http\Controllers\Controller;
use Illuminate\Support\Collection;
use Illuminate\Http\Request;
@ -166,7 +167,7 @@ class AccountImportController extends Controller
$accounts = [];
$now = \Carbon\Carbon::now();
$admins = $phones = [];
$admins = $phones = $passwords = [];
foreach ($lines as $line) {
if ($line->role == 'admin') {
@ -177,6 +178,10 @@ class AccountImportController extends Controller
$phones[$line->username] = $line->phone;
}
if (!empty($line->password)) {
$passwords[$line->username] = $line->password;
}
array_push($accounts, [
'username' => $line->username,
'domain' => $request->get('domain'),
@ -199,6 +204,31 @@ class AccountImportController extends Controller
$account->admin = true;
}
// Set passwords
$passwordsToInsert = [];
$passwordAccounts = Account::whereIn('username', array_keys($passwords))
->where('domain', $request->get('domain'))
->get();
$algorithm = config('app.account_default_password_algorithm');
foreach ($passwordAccounts as $passwordAccount) {
array_push($passwordsToInsert, [
'account_id' => $passwordAccount->id,
'password' => bchash(
$passwordAccount->username,
config('app.account_realm') ?? $request->get('domain'),
$passwords[$passwordAccount->username],
$algorithm
),
'algorithm' => $algorithm
]);
}
Password::insert($passwordsToInsert);
// Set admins accounts
foreach ($phones as $username => $phone) {
$account = Account::where('username', $username)

View file

@ -29,7 +29,7 @@ class ApiKeyController extends Controller
public function generate(Request $request)
{
$account = $request->user();
$account->generateApiKey($request);
$account->generateApiKey($request->ip());
$account->refresh();
Cookie::queue('x-api-key', $account->apiKey->key, config('app.api_key_expiration_minutes'));
@ -42,7 +42,7 @@ class ApiKeyController extends Controller
$authToken = AuthToken::where('token', $token)->valid()->firstOrFail();
if ($authToken->account) {
$authToken->account->generateApiKey($request);
$authToken->account->generateApiKey($request->ip());
$authToken->account->refresh();
Cookie::queue('x-api-key', $authToken->account->apiKey->key, config('app.api_key_expiration_minutes'));

View file

@ -26,6 +26,9 @@ use Carbon\Carbon;
use App\AccountCreationToken;
use App\AccountCreationRequestToken;
use App\Rules\PnParam;
use App\Rules\PnPrid;
use App\Rules\PnProvider;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Account\AuthenticateController as WebAuthenticateController;
use App\Libraries\FlexisipPusherConnector;
@ -36,9 +39,9 @@ class CreationTokenController extends Controller
public function sendByPush(Request $request)
{
$request->validate([
'pn_provider' => 'required',
'pn_param' => 'required',
'pn_prid' => 'required',
'pn_provider' => ['required', new PnProvider],
'pn_param' => [new PnParam],
'pn_prid' => [new PnPrid],
]);
$last = AccountCreationToken::where('pn_provider', $request->get('pn_provider'))

View file

@ -99,7 +99,12 @@ class AuthenticateJWT
return $next($request);
}
if (!empty(config('app.account_authentication_bearer'))) {
if (
!empty(config('app.account_authentication_bearer'))
// Bypass the JWT auth if we have an API Key
&& !$request->header('x-api-key')
&& !$request->cookie('x-api-key')
) {
$response = new Response();
$response->header(

View file

@ -11,7 +11,7 @@ class ValidateJSON
public function handle(Request $request, Closure $next)
{
if ($request->expectsJson()) {
if ($request->expectsJson() && !empty($request->getContent())) {
json_decode($request->getContent());
if (json_last_error() !== JSON_ERROR_NONE) {
abort(400, self::$message . ': ' . json_last_error_msg());

View file

@ -4,12 +4,13 @@ namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Respect\Validation\Validator;
use Propaganistas\LaravelPhone\PhoneNumber;
class IsNotPhoneNumber implements Rule
{
public function passes($attribute, $value): bool
{
return (Validator::not(Validator::phone())->not(Validator::startsWith('+'))->validate($value));
return (new PhoneNumber($value))->getCountry() == null;
}
public function message()

View file

@ -0,0 +1,19 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Respect\Validation\Validator;
class PnParam implements Rule
{
public function passes($attribute, $value)
{
return $value == null || Validator::regex('/^[\w\.]+$/')->validate($value);
}
public function message()
{
return 'The :attribute should be null or contain only alphanumeric, dots and underscore characters';
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Respect\Validation\Validator;
class PnPrid implements Rule
{
public function passes($attribute, $value)
{
return $value == null || Validator::regex('/^[\w\-\:]+$/')->validate($value);
}
public function message()
{
return 'The :attribute should be null or contain only alphanumeric, dashes and colon characters';
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Respect\Validation\Validator;
class PnProvider implements Rule
{
private $values = ['apns.dev', 'apns', 'fcm'];
public function passes($attribute, $value)
{
return in_array($value, $this->values);
}
public function message()
{
return 'The :attribute should be in ' . implode(', ', $this->values);
}
}

View file

@ -16,7 +16,7 @@
"laravel/framework": "^9.52",
"laravel/tinker": "^2.8",
"lcobucci/jwt": "^4.3",
"namoshek/laravel-redis-sentinel": "^0.1",
"namoshek/laravel-redis-sentinel": "^0.5",
"ovh/ovh": "^3.2",
"parsedown/laravel": "^1.2",
"phpunit/phpunit": "^9.6",
@ -24,7 +24,7 @@
"react/socket": "^1.14",
"respect/validation": "^2.2",
"sabre/vobject": "^4.5",
"scyllaly/hcaptcha": "^4.4"
"rvxlab/hcaptcha": "^4.4"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.9",

1230
flexiapi/composer.lock generated

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -236,7 +236,6 @@ return [
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
Scyllaly\HCaptcha\HCaptchaServiceProvider::class,
/*
* Package Service Providers...

View file

@ -1,4 +1,4 @@
Username,Password,Role,Status,Phone,Email
john,number9,user,active,+12341234,john@lennon.com
paul,a_day_in_the_life,admin,active,,paul@apple.com
ringo,allUneedIsL3ve,user,unactove,+123456,ringo@star.co.uk
ringo,allUneedIsL3ve,user,unactive,+123456,ringo@star.co.uk
1 Username Password Role Status Phone Email
2 john number9 user active +12341234 john@lennon.com
3 paul a_day_in_the_life admin active paul@apple.com
4 ringo allUneedIsL3ve user unactove unactive +123456 ringo@star.co.uk

View file

@ -488,6 +488,7 @@ header>h1 {
h1 i {
font-size: 3rem;
min-width: 3rem;
margin-right: 1rem;
}

View file

@ -38,7 +38,7 @@
{{ $account->apiKey->last_used_at }}
</td>
<td>
{{ $account->apiKey->ip ?? '-' }}
{{ $account->apiKey->ip ?? '*' }}
</td>
<td>
{{ $account->apiKey->requests }}

View file

@ -201,9 +201,9 @@ Return `503` if the token was not successfully sent.
JSON parameters:
* `pn_provider` the push notification provider
* `pn_param` the push notification parameter
* `pn_prid` the push notification unique id
* `pn_provider` **required**, the push notification provider, must be in apns.dev, apns or fcm
* `pn_param` the push notification parameter, can be null or contain only alphanumeric and underscore characters
* `pn_prid` the push notification unique id, can be null or contain only alphanumeric, dashes and colon characters
### `POST /account_creation_tokens/using-account-creation-request-token`
<span class="badge badge-success">Public</span>

View file

@ -64,13 +64,20 @@ class ApiAccountApiKeyTest extends TestCase
->json($this->method, '/api/accounts/me')
->assertStatus(200);
$this->keyAuthenticated($account)
->json($this->method, '/api/accounts/me')
->assertStatus(200);
// Bypass the JWT middleware
config()->set('app.account_authentication_bearer', 'fake-bearer');
$this->keyAuthenticated($account)
->json($this->method, '/api/accounts/me')
->assertStatus(200);
$this->assertDatabaseHas('api_keys', [
'account_id' => $account->id,
'requests' => 2
'requests' => 3
]);
DB::table('api_keys')->update(['ip' => 'no_localhost']);

View file

@ -36,7 +36,7 @@ class ApiAccountCreationTokenTest extends TestCase
protected $adminRoute = '/api/account_creation_tokens';
protected $method = 'POST';
protected $pnProvider = 'provider';
protected $pnProvider = 'fcm';
protected $pnParam = 'param';
protected $pnPrid = 'id';
@ -63,6 +63,27 @@ class ApiAccountCreationTokenTest extends TestCase
}
public function testCorrectParameters()
{
$this->assertSame(AccountCreationToken::count(), 0);
$this->json($this->method, $this->tokenRoute, [
'pn_provider' => 'wrong',
'pn_param' => $this->pnParam,
'pn_prid' => $this->pnPrid,
])->assertJsonValidationErrors(['pn_provider']);
$this->assertSame(AccountCreationToken::count(), 0);
$this->json($this->method, $this->tokenRoute, [
'pn_provider' => $this->pnProvider,
'pn_param' => '@wrong',
'pn_prid' => $this->pnPrid,
])->assertJsonValidationErrors(['pn_param']);
$this->assertSame(AccountCreationToken::count(), 0);
$this->json($this->method, $this->tokenRoute, [
'pn_provider' => $this->pnProvider,
'pn_param' => $this->pnParam,
'pn_prid' => '@wrong',
])->assertJsonValidationErrors(['pn_prid']);
$this->assertSame(AccountCreationToken::count(), 0);
$this->json($this->method, $this->tokenRoute, [
'pn_provider' => $this->pnProvider,
@ -173,8 +194,7 @@ class ApiAccountCreationTokenTest extends TestCase
'algorithm' => 'SHA-256',
'password' => '123',
'account_creation_token' => $token->token
])->assertStatus(422)
->assertJsonValidationErrors(['account_creation_token']);
])->assertJsonValidationErrors(['account_creation_token']);
}
public function testBlacklistedUsername()