From a20c0cff9ca6831e4a8fbdd00be51975a8806681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Jaussoin?= Date: Mon, 15 Dec 2025 11:32:00 +0100 Subject: [PATCH] Draft: Private Fix FLEXIAPI-408 Alstom hooks --- flexiapi/config/provisioning_hooks.php.alstom | 144 ++++++++++++++++++ flexiapi/tests/Feature/AccountAlstomTest.php | 123 +++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 flexiapi/config/provisioning_hooks.php.alstom create mode 100644 flexiapi/tests/Feature/AccountAlstomTest.php diff --git a/flexiapi/config/provisioning_hooks.php.alstom b/flexiapi/config/provisioning_hooks.php.alstom new file mode 100644 index 0000000..059ce22 --- /dev/null +++ b/flexiapi/config/provisioning_hooks.php.alstom @@ -0,0 +1,144 @@ +parse($request->bearerToken()); + + if ($token->claims()->has('matching_accounts')) { + $matchingAccounts = $token->claims()->get('matching_accounts'); + if ( + is_array($matchingAccounts) + && !empty($matchingAccounts) + ) { + $firstMatchingAccount = array_shift($matchingAccounts); + + if (\str_contains($firstMatchingAccount, '@')) { + list($username, $domain) = explode('@', substr($firstMatchingAccount, 4)); + + $accounts = Account::withoutGlobalScopes()->where(['username' => $username, 'domain' => $domain]); + + foreach ($matchingAccounts as $sip) { + if (\str_contains($sip, '@')) { + list($username, $domain) = explode('@', substr($sip, 4)); + $accounts = $accounts->orWhere( + fn($query) => $query + ->where('username', $username) + ->where('domain', $domain) + ); + } + } + + $accounts = $accounts->get(); + + if ($accounts->count() == count($matchingAccounts) + 1) { + // Resolving the first Asterisk offline account from the list + $resolvedAccount = null; + + $resolvedAccount = $accounts->first(); + /* foreach ($accounts as $account) { + $response = Http::withOptions([ + 'verify' => ASTERISK_PATH_CERFIFICATE + ])->get(ASTERISK_ARI_ROOT_URL . 'PJSIP/' . $account->identifier); // account SIP address + + if ($response->json('state') == 'offline') { + $resolvedAccount = $account; + break; + } + }*/ + + if ($resolvedAccount) { + Log::channel('events')->info( + 'Alstom Account: Account provisioned', + ['id' => $resolvedAccount->identifier] + ); + + $xpath = new \DOMXpath($authSection->ownerDocument); + + $xpath->query("//entry[@name='reg_identity']")->item(0)->nodeValue = $resolvedAccount->fullIdentifier; + + $xpath->query("//entry[@name='username']")->item(0)->nodeValue = $resolvedAccount->username; + $xpath->query("//entry[@name='domain']")->item(0)->nodeValue = $resolvedAccount->domain; + + $password = $resolvedAccount->passwords()->first(); + + $xpath->query("//entry[@name='ha1']")->item(0)->nodeValue = $password->password; + $xpath->query("//entry[@name='realm']")->item(0)->nodeValue = $resolvedAccount->resolvedRealm; + $xpath->query("//entry[@name='algorithm']")->item(0)->nodeValue = $password->algorithm; + return; + } + + Log::channel('events')->info( + 'Alstom Account: No account can be provisioned', + ['id' => $token->claims()->get('matching_accounts')] + ); + abort(404, 'No account can be provisioned'); + } + + Log::channel('events')->info( + 'Alstom Account: No account can be provisioned', + ['id' => $token->claims()->get('matching_accounts')] + ); + abort(400, 'Listed matching_accounts are not present in the database'); + } + + abort(400, 'Invalid matching_accounts format'); + return; + } + + Log::channel('events')->info( + 'Alstom Account: matching_accounts is empty or invalid' + ); + abort(400, 'matching_accounts is empty or invalid'); + return; + } + + Log::channel('events')->info( + 'Alstom Account: matching_accounts element missing' + ); + abort(400, 'matching_accounts element missing'); +} + +/** + * @brief Complete the proxy section XML node, the Account might be passed as a parameter if resolved + * @param DOMElement $proxySection + * @param Request $request + * @param Account $account + * @return void + */ +function provisioningAdditionalSectionHook(\DOMElement $config, Request $request, ?Account $account) +{ +} \ No newline at end of file diff --git a/flexiapi/tests/Feature/AccountAlstomTest.php b/flexiapi/tests/Feature/AccountAlstomTest.php new file mode 100644 index 0000000..95d8302 --- /dev/null +++ b/flexiapi/tests/Feature/AccountAlstomTest.php @@ -0,0 +1,123 @@ +. +*/ + +namespace Tests\Feature; + +use App\Password; +use DateTimeImmutable; +use Lcobucci\Clock\FrozenClock; +use Lcobucci\JWT\Builder; +use Lcobucci\JWT\JwtFacade; +use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\Signer\Rsa\Sha256; + +class AccountAlstomTest extends AccountJWTAuthenticationTest +{ + + public function setUp(): void + { + parent::setUp(); + } + + public function testAlstomProvisioning() + { + # JWT is disabled if Sodium is not loaded + if (!extension_loaded('sodium')) + return; + + $password = Password::factory()->create(); + + //$domain = 'sip_provisioning.example.com'; + $bearer = 'authz_server="https://sso.test/", realm="sip.test.org"'; + + //\App\Space::where('domain', $password->account->domain)->update(['host' => $domain]); + //config()->set('app.sip_domain', $domain); + config()->set('services.jwt.rsa_public_key_pem', $this->serverPublicKeyPem); + + $this->get($this->route)->assertStatus(400); + + // Accounts to provision + $passwordAccount1 = Password::factory()->create(); + $passwordAccount2 = Password::factory()->create(); + + $clock = new FrozenClock(new DateTimeImmutable()); + + config()->set('services.jwt.sip_identifier', 'sip_identity'); + + $token = (new JwtFacade(null, $clock))->issue( + new Sha256(), + InMemory::plainText($this->serverPrivateKeyPem), + static fn( + Builder $builder, + DateTimeImmutable $issuedAt + ): Builder => $builder + ->withClaim( + 'sip_identity', + 'sip:' . $password->account->username . '@' . $password->account->domain + ) + ->withClaim( + 'matching_accounts', + [ + 'sip:' . $passwordAccount1->account->identifier, + 'sip:' . $passwordAccount2->account->identifier + ] + ) + ); + + $this->withHeaders([ + 'Authorization' => 'Bearer ' . $token->toString(), + 'x-linphone-provisioning' => true, + ]) + ->get($this->accountRoute) + ->assertStatus(200) + ->assertSee($passwordAccount1->account->username) + ->assertSee($passwordAccount1->account->passwords()->first()->ha1); + + // Non existing accounts + + $token = (new JwtFacade(null, $clock))->issue( + new Sha256(), + InMemory::plainText($this->serverPrivateKeyPem), + static fn( + Builder $builder, + DateTimeImmutable $issuedAt + ): Builder => $builder + ->withClaim( + 'sip_identity', + 'sip:' . $password->account->username . '@' . $password->account->domain + ) + ->withClaim( + 'matching_accounts', + [ + 'sip:' . $passwordAccount1->account->identifier, + 'sip:' . $passwordAccount2->account->identifier, + 'sip:other@account.com' + ] + ) + ); + + $this->withHeaders([ + 'Authorization' => 'Bearer ' . $token->toString(), + 'x-linphone-provisioning' => true, + ]) + ->get($this->accountRoute) + ->assertStatus(400) + ->dump(); + } +}