-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Fixes #7237 allow modules to add CLI commands. We change up the OpenEMR command runner to be deprecated and add a warning notice for people to use the OpenEMR symfony command runner in bin/console. Changed up the symfony command runner to use a class that also fires an event to grab module commands if there exist any in the system. Deprecated the create client assertions command since that is used in our FHIR api documentation. I completely migrated the api documentation creation command into the symfony command runner. * Fix copyright notice for paid work. * Fix command description
- Loading branch information
Showing
6 changed files
with
292 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
src/Common/Command/CreateClientCredentialsAssertionSymfonyCommand.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
<?php | ||
|
||
/** | ||
* CreateClientCredentialsAssertion Is a helper utility to create a Client Credentials Grant assertion statement as | ||
* well as print out the Public JSON Web Key Set that can be used for a test System App. | ||
* | ||
* @package openemr | ||
* @link http://www.open-emr.org | ||
* @author Stephen Nielson <stephen@nielson.org> | ||
* @copyright Copyright (c) 2021 Stephen Nielson <stephen@nielson.org> | ||
* @copyright Copyright (c) 2024 Care Management Solutions, Inc. <stephen.waite@cmsvt.com> | ||
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 | ||
*/ | ||
|
||
namespace OpenEMR\Common\Command; | ||
|
||
use Lcobucci\JWT\Configuration; | ||
use Lcobucci\JWT\Signer\Key\InMemory; | ||
use Lcobucci\JWT\Signer\Rsa\Sha384; | ||
use OpenEMR\Common\Auth\OpenIDConnect\Grant\CustomClientCredentialsGrant; | ||
use OpenEMR\Common\Command\Runner\CommandContext; | ||
use Ramsey\Uuid\Uuid; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\Console\Input\InputDefinition; | ||
use Symfony\Component\Console\Input\InputInterface; | ||
use Symfony\Component\Console\Input\InputOption; | ||
use Symfony\Component\Console\Output\OutputInterface; | ||
|
||
class CreateClientCredentialsAssertionSymfonyCommand extends Command | ||
{ | ||
protected function configure() | ||
{ | ||
$this | ||
->setName('openemr:create-client-credentials-assertion') | ||
->setDescription("Utility class to help test and use the client credentials grant assertion") | ||
->addUsage('--site=default') | ||
->setDefinition( | ||
new InputDefinition([ | ||
new InputOption('issuer', 'i', InputOption::VALUE_REQUIRED, 'JSON Web Token (JWT) Issuer. This should be the The Client ID received from the OpenEMR registration. Used as the issuer and subject for the JWT'), | ||
new InputOption('oauth-token-url', 'a', InputOption::VALUE_REQUIRED, 'OpenEMR OAuth2 Token URL is the audience of the JWT', 'https://localhost:9300/default/oauth2/token'), | ||
new InputOption('print-jwks', 'k', InputOption::VALUE_NONE, 'Prints out the JSON Web Key Set public key that can be registered with OpenEMR for the client application'), | ||
]) | ||
); | ||
} | ||
|
||
/** | ||
* Runs the command | ||
* @param CommandContext $context | ||
* @throws \Exception | ||
*/ | ||
protected function execute(InputInterface $input, OutputInterface $output): int | ||
{ | ||
$rootPath = $GLOBALS['fileroot']; | ||
|
||
$keyLocation = $rootPath . DIRECTORY_SEPARATOR . "tests" . DIRECTORY_SEPARATOR . "Tests" . DIRECTORY_SEPARATOR | ||
. "data" . DIRECTORY_SEPARATOR . "Unit" . DIRECTORY_SEPARATOR . "Common" . DIRECTORY_SEPARATOR . "Auth" | ||
. DIRECTORY_SEPARATOR . "Grant" . DIRECTORY_SEPARATOR; | ||
|
||
if ($input->getOption('print-jwks')) { | ||
$jwks = file_get_contents($keyLocation . "jwk-public-valid.json"); | ||
$output->writeln("JSON Web Key Set (Public Key)"); | ||
$output->writeln("WARNING - THIS IS FOR TESTING PURPOSES ONLY!"); | ||
$output->writeln("DO NOT USE THIS IN PRODUCTION AS THE PRIVATE KEYS FOR THIS JWKS IS COMMITED TO THE SOURCE CODE\n"); | ||
$output->writeln($jwks . "\n"); | ||
return Command::SUCCESS; | ||
} | ||
$clientId = $input->getOption('issuer'); | ||
$oauthTokenUrl = $input->getOption('oauth-token-url'); | ||
if (empty($clientId) || empty($oauthTokenUrl)) { | ||
$output->writeln("Missing required arguments."); | ||
$output->writeln($this->getSynopsis()); | ||
return Command::FAILURE; | ||
} | ||
|
||
$configuration = Configuration::forAsymmetricSigner( | ||
// You may use RSA or ECDSA and all their variations (256, 384, and 512) | ||
new Sha384(), | ||
InMemory::file($keyLocation . "openemr-rsa384-private.key"), | ||
InMemory::file($keyLocation . "openemr-rsa384-public.pem") | ||
// You may also override the JOSE encoder/decoder if needed by providing extra arguments here | ||
); | ||
|
||
$jti = Uuid::uuid4(); | ||
|
||
$now = new \DateTimeImmutable(); | ||
$token = $configuration->builder() | ||
// Configures the issuer (iss claim) | ||
->issuedBy($clientId) | ||
// Configures the audience (aud claim) | ||
->permittedFor($oauthTokenUrl) | ||
// Configures the id (jti claim) | ||
->identifiedBy($jti) | ||
// Configures the time that the token was issue (iat claim) | ||
->issuedAt($now) | ||
// Configures the time that the token can be used (nbf claim) | ||
->canOnlyBeUsedAfter($now) | ||
// Configures the expiration time of the token (exp claim) | ||
->expiresAt($now->modify('+60 seconds')) | ||
->relatedTo($clientId) | ||
->getToken($configuration->signer(), $configuration->signingKey()); | ||
$assertion = $token->toString(); // The string representation of the object is a JWT string | ||
$output->writeln("Generated Client Credentials Assertion"); | ||
$output->writeln($assertion); | ||
|
||
$output->writeln("\n\nSample CURL request using assertion: "); | ||
$assertionType = CustomClientCredentialsGrant::OAUTH_JWT_CLIENT_ASSERTION_TYPE; | ||
$scope = 'system/*.\$export system/*.\$bulkdata-status system/Group.\$export system/Patient.\$export ' | ||
. 'system/Encounter.read system/Binary.read'; | ||
$output->writeln("--> curl -k -X POST --data-urlencode \"client_assertion_type=$assertionType\" \\\n" | ||
. " --data-urlencode \"client_assertion=$assertion\" \\\n" | ||
. " --data-urlencode \"grant_type=client_credentials\" \\\n" | ||
. " --data-urlencode \"scope=$scope\" $oauthTokenUrl"); | ||
return Command::SUCCESS; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
<?php | ||
|
||
/** | ||
* Wraps around the symfony console command runner and allows module writers to add commands that are | ||
* in the system. | ||
* | ||
* @package OpenEMR | ||
* @link http://www.open-emr.org | ||
* | ||
* @author Stephen Nielson <snielson@discoverandchange.com> | ||
* @copyright Copyright (c) 2024 Care Management Solutions, Inc. <stephen.waite@cmsvt.com> | ||
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 | ||
*/ | ||
|
||
namespace OpenEMR\Common\Command; | ||
|
||
use OpenEMR\Common\Command\Runner\AclModify; | ||
use OpenEMR\Common\Command\Runner\CcdaImport; | ||
use OpenEMR\Common\Command\Runner\CcdaNewpatient; | ||
use OpenEMR\Common\Command\Runner\CcdaNewpatientImport; | ||
use OpenEMR\Common\Command\Runner\IOpenEMRCommand; | ||
use OpenEMR\Common\Command\Runner\Register; | ||
use OpenEMR\Common\Command\Runner\ZfcModule; | ||
use OpenEMR\Events\Command\CommandRunnerFilterEvent; | ||
use Symfony\Component\Console\Application; | ||
use Symfony\Component\Console\Command\Command; | ||
use Symfony\Component\EventDispatcher\EventDispatcher; | ||
use Symfony\Component\Finder\Finder; | ||
|
||
class SymfonyCommandRunner | ||
{ | ||
private $eventDispatcher; | ||
|
||
public function __construct() | ||
{ | ||
} | ||
public function setEventDispatcher(EventDispatcher $eventDispatcher) | ||
{ | ||
$this->eventDispatcher = $eventDispatcher; | ||
} | ||
public function getEventDispatcher(): EventDispatcher | ||
{ | ||
if (!isset($this->eventDispatcher)) { | ||
$this->eventDispatcher = $GLOBALS['kernel']->getEventDispatcher(); | ||
} | ||
return $this->eventDispatcher; | ||
} | ||
|
||
public function run() | ||
{ | ||
$commands = $this->findCommands(); | ||
$app = new Application(); | ||
foreach ($commands as $command) { | ||
$app->add($command); | ||
} | ||
$app->run(); | ||
} | ||
|
||
/** | ||
* @return Command[] | ||
*/ | ||
private function findCommands(): array | ||
{ | ||
try { | ||
$finder = new Finder(); | ||
$files = $finder->files()->in(__DIR__)->name("*.php"); | ||
$filterCommand = new CommandRunnerFilterEvent(); | ||
foreach ($files as $file) { | ||
$fileName = $file->getFilenameWithoutExtension(); | ||
$fqn = __NAMESPACE__ . "\\" . $fileName; | ||
if (empty($fileName) || $fqn == self::class) { | ||
continue; // skip over ourselves | ||
} | ||
if (class_exists($fqn)) { | ||
$command = new $fqn(); | ||
if ($command instanceof Command) { | ||
$filterCommand->setCommand($command::class, $command); | ||
} | ||
} | ||
} | ||
// dispatch an event so modules can also add commands | ||
$this->getEventDispatcher()->dispatch($filterCommand, CommandRunnerFilterEvent::EVENT_NAME); | ||
return $filterCommand->getCommands(); | ||
} catch (\Exception $ex) { | ||
echo "Error in attempting to find commands " . $ex->getMessage() . "\n"; | ||
die(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
<?php | ||
|
||
/** | ||
* Used for adding / modifying / removing commands that are used in the core OpenEMR command runner | ||
* IE php bin/console. @see https://symfony.com/doc/current/console.html for documentation on how | ||
* the command runner is run. As a convention module writers should prefix their commands with a namespace name | ||
* and NOT use openemr:<command-name> to clearly differentiate a module's commands from the core command API. | ||
* | ||
* @package OpenEMR | ||
* @link http://www.open-emr.org | ||
* | ||
* @author Stephen Nielson <snielson@discoverandchange.com> | ||
* @copyright Copyright (c) 2024 Care Management Solutions, Inc. <stephen.waite@cmsvt.com> | ||
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3 | ||
*/ | ||
|
||
namespace OpenEMR\Events\Command; | ||
|
||
use Symfony\Component\Console\Command\Command; | ||
|
||
class CommandRunnerFilterEvent | ||
{ | ||
const EVENT_NAME = "openemr.command-runner.filter"; | ||
private $commands = []; | ||
|
||
public function __construct() | ||
{ | ||
$this->commands = []; | ||
} | ||
|
||
public function getCommands(): array | ||
{ | ||
return $this->commands; // creates a copy of the arrays | ||
} | ||
|
||
public function setCommand($fqdn, Command $command) | ||
{ | ||
$this->commands[$fqdn] = $command; | ||
} | ||
public function hasCommand($fqdn): bool | ||
{ | ||
return isset($this->commands[$fqdn]); | ||
} | ||
|
||
public function removeCommand($command) | ||
{ | ||
$commandFQDN = $command; | ||
if ($command instanceof Command) { | ||
$commandFQDN = $command::class; | ||
} | ||
|
||
if ($this->hasCommand($commandFQDN)) { | ||
unset($this->commands[$commandFQDN]); | ||
} else { | ||
throw new \InvalidArgumentException("Passed in argument is not in list of commands"); | ||
} | ||
} | ||
} |