Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: publish to two solar tenants #398

4 changes: 2 additions & 2 deletions config/default/PlatformKeyChainRepository.conf.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

use oat\taoLti\models\classes\Security\DataAccess\Repository\PlatformKeyChainRepository;

return new PlatformKeyChainRepository(
return new PlatformKeyChainRepository([
[
PlatformKeyChainRepository::OPTION_DEFAULT_KEY_ID => 'defaultPlatformKeyId',
PlatformKeyChainRepository::OPTION_DEFAULT_KEY_NAME => 'defaultPlatformKeyName',
PlatformKeyChainRepository::OPTION_DEFAULT_PUBLIC_KEY_PATH => '/platform/default/public.key',
PlatformKeyChainRepository::OPTION_DEFAULT_PRIVATE_KEY_PATH => '/platform/default/private.key',
]
);
]);
34 changes: 34 additions & 0 deletions migrations/Version202402271423013774_taoLti.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace oat\taoLti\migrations;

use Doctrine\DBAL\Schema\Schema;
use oat\tao\model\security\xsrf\TokenService;
use oat\tao\scripts\tools\migrations\AbstractMigration;
use oat\taoLti\models\classes\Security\DataAccess\Repository\PlatformKeyChainRepository;

final class Version202402271423013774_taoLti extends AbstractMigration
{
public function getDescription(): string
{
return 'Update PlatformKeyChain config format';
}

public function up(Schema $schema): void
{
$platformKeyChainRepository = $this->getServiceLocator()->get(PlatformKeyChainRepository::SERVICE_ID);
$options = $platformKeyChainRepository->getOptions();
$platformKeyChainRepository->setOptions([$options]);
$this->getServiceLocator()->register(PlatformKeyChainRepository::SERVICE_ID, $platformKeyChainRepository);
}

public function down(Schema $schema): void
{
$platformKeyChainRepository = $this->getServiceLocator()->get(PlatformKeyChainRepository::SERVICE_ID);
$options = $platformKeyChainRepository->getOptions();
$platformKeyChainRepository->setOptions(reset($options));
$this->getServiceLocator()->register(PlatformKeyChainRepository::SERVICE_ID, $platformKeyChainRepository);
}
}
27 changes: 27 additions & 0 deletions models/classes/Exception/PlatformKeyChainException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2024 (original work) Open Assessment Technologies SA;
*/

namespace oat\taoLti\models\classes\Exception;

use oat\taoLti\models\classes\LtiException;

class PlatformKeyChainException extends LtiException
{
}
21 changes: 15 additions & 6 deletions models/classes/Platform/Service/CachedKeyChainGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
namespace oat\taoLti\models\classes\Platform\Service;

use OAT\Library\Lti1p3Core\Security\Key\KeyChainInterface;
use OAT\Library\Lti1p3Core\Security\Key\KeyChainRepositoryInterface;
use oat\oatbox\cache\SimpleCache;
use oat\oatbox\service\ConfigurableService;
use oat\taoLti\models\classes\Security\DataAccess\Repository\CachedPlatformJwksRepository;
Expand All @@ -33,15 +32,25 @@

class CachedKeyChainGenerator extends ConfigurableService implements KeyChainGeneratorInterface
{
public function generate(): KeyChainInterface

public function generate(
string $id = PlatformKeyChainRepository::OPTION_DEFAULT_KEY_ID_VALUE,
string $name = PlatformKeyChainRepository::OPTION_DEFAULT_KEY_NAME_VALUE
): KeyChainInterface {
$keyChain = $this->getKeyChainGenerator()->generate($id, $name);
$this->save($keyChain);

return $keyChain;
}

private function save(KeyChainInterface $keyChain): bool
{
$keyChain = $this->getKeyChainGenerator()->generate();
$this->getKeyChainRepository()->save($keyChain);
$this->getKeyChainRepository()->saveKeyChain($keyChain);

$this->invalidateKeyChain($keyChain);
$this->invalidateJwks();

return $keyChain;
return true;
}

private function invalidateKeyChain(KeyChainInterface $keyChain): void
Expand All @@ -65,7 +74,7 @@ private function getKeyChainGenerator(): KeyChainGeneratorInterface
return $this->getServiceLocator()->get(OpenSslKeyChainGenerator::class);
}

private function getKeyChainRepository(): KeyChainRepositoryInterface
private function getKeyChainRepository(): PlatformKeyChainRepository
{
return $this->getServiceLocator()->get(PlatformKeyChainRepository::class);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ interface KeyChainGeneratorInterface
{
public const OPTION_DATA_STORE = 'sslConfig';

public function generate(): KeyChainInterface;
public function generate(string $id, string $name): KeyChainInterface;
}
10 changes: 6 additions & 4 deletions models/classes/Platform/Service/OpenSslKeyChainGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@

class OpenSslKeyChainGenerator extends ConfigurableService implements KeyChainGeneratorInterface
{
public function generate(): KeyChainInterface
{
public function generate(
string $id = PlatformKeyChainRepository::OPTION_DEFAULT_KEY_ID,
string $name = PlatformKeyChainRepository::OPTION_DEFAULT_KEY_NAME
): KeyChainInterface {
$resource = openssl_pkey_new($this->getOption(self::OPTION_DATA_STORE));
openssl_pkey_export($resource, $privateKey);
$publicKey = openssl_pkey_get_details($resource);

return new KeyChain(
PlatformKeyChainRepository::OPTION_DEFAULT_KEY_ID,
PlatformKeyChainRepository::OPTION_DEFAULT_KEY_NAME,
$id,
$name,
new Key($publicKey['key']),
new Key($privateKey)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,19 @@ class CachedPlatformKeyChainRepository extends ConfigurableService implements Ke
* @throws InvalidArgumentException
* @throws ErrorException
*/
public function save(KeyChainInterface $keyChain): void
public function saveDefaultKeyChain(KeyChainInterface $keyChain): void
{
$this->setKeys(
$keyChain,
$keyChain->getIdentifier()
);

$this->getPlatformKeyChainRepository()->save($keyChain);
$this->getPlatformKeyChainRepository()->saveDefaultKeyChain($keyChain);
}

public function find(string $identifier): ?KeyChainInterface
{
if ($this->exists($identifier)) {
//TODO: Needs to be refactor if we have multiple key chains
$rawKeys = $this->getCacheService()->getMultiple(
[
sprintf(self::PRIVATE_PATTERN, $identifier),
Expand Down Expand Up @@ -93,7 +92,6 @@ public function findAll(KeyChainQuery $query): KeyChainCollection
}

if ($this->exists($query->getIdentifier())) {
//TODO: Needs to be refactor if we have multiple key chains
$rawKeys = $this->getCacheService()->getMultiple(
[
sprintf(self::PRIVATE_PATTERN, $query->getIdentifier()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,102 +23,139 @@
namespace oat\taoLti\models\classes\Security\DataAccess\Repository;

use common_exception_NoImplementation;
use ErrorException;
use League\Flysystem\FilesystemInterface;
use OAT\Library\Lti1p3Core\Security\Key\Key;
use OAT\Library\Lti1p3Core\Security\Key\KeyChain;
use OAT\Library\Lti1p3Core\Security\Key\KeyChainInterface;
use OAT\Library\Lti1p3Core\Security\Key\KeyChainRepositoryInterface;
use oat\oatbox\filesystem\FileSystemService;
use oat\oatbox\service\ConfigurableService;
use oat\tao\model\security\Business\Domain\Key\KeyChainCollection;
use oat\tao\model\security\Business\Domain\Key\KeyChainQuery;
use oat\tao\model\security\Business\Domain\Key\Key as TaoKey;
use oat\tao\model\security\Business\Domain\Key\KeyChain as TaoKeyChain;
use oat\tao\model\security\Business\Domain\Key\KeyChainCollection;
use oat\tao\model\security\Business\Domain\Key\KeyChainQuery;
use oat\taoLti\models\classes\Exception\PlatformKeyChainException;

class PlatformKeyChainRepository extends ConfigurableService implements KeyChainRepositoryInterface
{
public const SERVICE_ID = 'taoLti/PlatformKeyChainRepository';
public const OPTION_DEFAULT_KEY_ID = 'defaultKeyId';
public const OPTION_DEFAULT_KEY_ID_VALUE = 'defaultPlatformKeyId';
public const OPTION_DEFAULT_KEY_NAME = 'defaultKeyName';
public const OPTION_DEFAULT_KEY_NAME_VALUE = 'defaultPlatformKeyName';
public const OPTION_DEFAULT_PUBLIC_KEY_PATH = 'defaultPublicKeyPath';
public const OPTION_DEFAULT_PRIVATE_KEY_PATH = 'defaultPrivateKeyPath';
public const FILE_SYSTEM_ID = 'ltiKeyChain';

/**
* @throws ErrorException
*/
public function save(KeyChainInterface $keyChain): void

public function saveDefaultKeyChain(KeyChainInterface $keyChain): void
{
$this->save($keyChain, $this->getDefaultKeyId());
}

public function saveKeyChain(KeyChainInterface $keyChain): void
{
$this->save($keyChain, $keyChain->getIdentifier());
}

protected function save(KeyChainInterface $keyChain, string $identifier): void
{
$isPublicKeySaved = $this->getFileSystem()
->put(
ltrim($this->getOption(self::OPTION_DEFAULT_PUBLIC_KEY_PATH), DIRECTORY_SEPARATOR),
$keyChain->getPublicKey()->getContent()
);

$isPrivateKeySaved = $this->getFileSystem()
->put(
ltrim($this->getOption(self::OPTION_DEFAULT_PRIVATE_KEY_PATH), DIRECTORY_SEPARATOR),
$keyChain->getPrivateKey()->getContent()
);
$configs = $this->findConfiguration($identifier);

if (empty($configs)) {
throw new PlatformKeyChainException('Impossible to write LTI keys. Configuration not found');
}

$publicKey = $configs[self::OPTION_DEFAULT_PUBLIC_KEY_PATH] ?? null;
$privateKey = $configs[self::OPTION_DEFAULT_PRIVATE_KEY_PATH] ?? null;
$isPublicKeySaved = null;
$isPrivateKeySaved = null;

if ($publicKey !== null && $privateKey !== null) {
$isPublicKeySaved = $this->getFileSystem()
->put(
ltrim($publicKey, DIRECTORY_SEPARATOR),
$keyChain->getPublicKey()->getContent()
);

$isPrivateKeySaved = $this->getFileSystem()
->put(
ltrim($privateKey, DIRECTORY_SEPARATOR),
$keyChain->getPrivateKey()->getContent()
);
}

if (!$isPublicKeySaved || !$isPrivateKeySaved) {
throw new ErrorException('Impossible to write LTI keys');
throw new PlatformKeyChainException('Impossible to write LTI keys');
}
}

public function getDefaultKeyId(): string
{
return $this->getOption(PlatformKeyChainRepository::OPTION_DEFAULT_KEY_ID, '');
$options = $this->getOptions();
return reset($options)[self::OPTION_DEFAULT_KEY_ID] ?? '';
}

/**
* @throws common_exception_NoImplementation
*/
public function find(string $identifier): ?KeyChainInterface
{
if ($identifier !== $this->getDefaultKeyId()) {
$configs = $this->findConfiguration($identifier);

if (empty($configs)) {
return null;
}

$publicKey = $this->getFileSystem()
->read($this->getOption(self::OPTION_DEFAULT_PUBLIC_KEY_PATH));
$publicKeyPath = $configs[self::OPTION_DEFAULT_PUBLIC_KEY_PATH] ?? null;
$privateKeyPath = $configs[self::OPTION_DEFAULT_PRIVATE_KEY_PATH] ?? null;

if (!$publicKeyPath || !$privateKeyPath) {
throw new PlatformKeyChainException('The key path is not defined');
}

$privateKey = $this->getFileSystem()
->read($this->getOption(self::OPTION_DEFAULT_PRIVATE_KEY_PATH));
$publicKey = $this->getFileSystem()->read($configs[self::OPTION_DEFAULT_PUBLIC_KEY_PATH] ?? null);
$privateKey = $this->getFileSystem()->read($configs[self::OPTION_DEFAULT_PRIVATE_KEY_PATH] ?? null);
mccar marked this conversation as resolved.
Show resolved Hide resolved

if ($publicKey === false || $privateKey === false) {
throw new ErrorException('Impossible to read LTI keys');
throw new PlatformKeyChainException('Impossible to read LTI keys');
}

return new KeyChain(
$this->getDefaultKeyId(),
$this->getOption(self::OPTION_DEFAULT_KEY_NAME),
$configs[self::OPTION_DEFAULT_KEY_ID] ?? null,
$configs[self::OPTION_DEFAULT_KEY_NAME] ?? null,
new Key($publicKey),
new Key($privateKey)
mccar marked this conversation as resolved.
Show resolved Hide resolved
);
}

public function findAll(KeyChainQuery $query): KeyChainCollection
{
$publicKey = $this->getFileSystem()
->read($this->getOption(self::OPTION_DEFAULT_PUBLIC_KEY_PATH));

$privateKey = $this->getFileSystem()
->read($this->getOption(self::OPTION_DEFAULT_PRIVATE_KEY_PATH));

if ($publicKey === false || $privateKey === false) {
throw new ErrorException('Impossible to read LTI keys');
$options = $this->getOptions();
foreach ($options as $configs) {
$defaultKeyId = $configs[self::OPTION_DEFAULT_KEY_ID] ?? null;
$defaultKeyName = $configs[self::OPTION_DEFAULT_KEY_NAME] ?? null;
$publicKeyPath = $configs[self::OPTION_DEFAULT_PUBLIC_KEY_PATH] ?? null;
$privateKeyPath = $configs[self::OPTION_DEFAULT_PRIVATE_KEY_PATH] ?? null;

if ($defaultKeyId && $publicKeyPath && $privateKeyPath) {
$publicKey = $this->getFileSystem()->read($publicKeyPath);
$privateKey = $this->getFileSystem()->read($privateKeyPath);

$keyChains[] = new TaoKeyChain(
$defaultKeyId,
$defaultKeyName,
new TaoKey($publicKey),
new TaoKey($privateKey)
);
}
}

$keyChain = new TaoKeyChain(
$this->getOption(self::OPTION_DEFAULT_KEY_ID),
$this->getOption(self::OPTION_DEFAULT_KEY_NAME),
new TaoKey($publicKey),
new TaoKey($privateKey)
);
if (empty($keyChains)) {
throw new PlatformKeyChainException('Impossible to read LTI keys');
}

return new KeyChainCollection($keyChain);
return new KeyChainCollection($keyChains);
mccar marked this conversation as resolved.
Show resolved Hide resolved
}


Expand All @@ -138,4 +175,20 @@ private function getFileSystem(): FilesystemInterface

return $fileSystemService->getFileSystem(self::FILE_SYSTEM_ID);
}

/**
* @param string $identifier
* @return array|null
*/
protected function findConfiguration(string $identifier): ?array
{
$options = $this->getOptions();
foreach ($options as $configs) {
if ($configs[self::OPTION_DEFAULT_KEY_ID] === $identifier) {
return $configs;
}
}

return null;
}
}
Loading
Loading