Skip to content

Commit

Permalink
feat: publish to two solar tenants (#399)
Browse files Browse the repository at this point in the history
* feat: publish to two solar tenants

---------

Co-authored-by: Makar Sichevoi <makar.sichevoy@taotesting.com>
  • Loading branch information
mccar and Makar Sichevoi authored Mar 9, 2024
1 parent 52fe7b7 commit b44cb0f
Show file tree
Hide file tree
Showing 14 changed files with 410 additions and 90 deletions.
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,
?string $passPhrase = null
): KeyChainInterface {
$keyChain = $this->getKeyChainGenerator()->generate($id, $name, $passPhrase);
$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, ?string $passPhrase): KeyChainInterface;
}
15 changes: 9 additions & 6 deletions models/classes/Platform/Service/OpenSslKeyChainGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,20 @@

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,
?string $passPhrase = null
): KeyChainInterface {
$resource = openssl_pkey_new($this->getOption(self::OPTION_DATA_STORE));
openssl_pkey_export($resource, $privateKey);
openssl_pkey_export($resource, $privateKey, $passPhrase);
$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)
new Key($privateKey, $passPhrase)
);
}
}
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,140 @@
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 OPTION_DEFAULT_PRIVATE_KEY_PASSPHRASE = 'defaultPrivateKeyPassphrase';
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
{
$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()
);
$this->save($keyChain, $keyChain->getIdentifier());
}

protected function save(KeyChainInterface $keyChain, string $identifier): void
{
$configs = $this->findConfiguration($identifier);

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

$publicKeyPath = $configs[self::OPTION_DEFAULT_PUBLIC_KEY_PATH] ?? null;
$privateKeyPath = $configs[self::OPTION_DEFAULT_PRIVATE_KEY_PATH] ?? null;
$isPublicKeySaved = false;
$isPrivateKeySaved = false;

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

$isPrivateKeySaved = $this->getFileSystem()
->put(
ltrim($privateKeyPath, 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));
$keyName = $configs[self::OPTION_DEFAULT_KEY_NAME] ?? '';
$publicKeyPath = $configs[self::OPTION_DEFAULT_PUBLIC_KEY_PATH] ?? null;
$privateKeyPath = $configs[self::OPTION_DEFAULT_PRIVATE_KEY_PATH] ?? null;
$privateKeyPassphrase = $configs[self::OPTION_DEFAULT_PRIVATE_KEY_PASSPHRASE] ?? 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($publicKeyPath);
$privateKey = $this->getFileSystem()->read($privateKeyPath);

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),
$identifier,
$keyName,
new Key($publicKey),
new Key($privateKey)
new Key($privateKey, $privateKeyPassphrase)
);
}

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] ?? '';
$publicKeyPath = $configs[self::OPTION_DEFAULT_PUBLIC_KEY_PATH] ?? null;
$privateKeyPath = $configs[self::OPTION_DEFAULT_PRIVATE_KEY_PATH] ?? null;
$privateKeyPassphrase = $configs[self::OPTION_DEFAULT_PRIVATE_KEY_PASSPHRASE] ?? 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, $privateKeyPassphrase)
);
}
}

$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);
}


Expand All @@ -138,4 +176,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

0 comments on commit b44cb0f

Please sign in to comment.