diff --git a/docs/_sidebar.md b/docs/_sidebar.md index b41d3604..80a4a973 100755 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -67,7 +67,9 @@ - [`healthcheck`](/commands/system/healthcheck) - **User Commands** - [`user:create`](/commands/user/user-create) - - [`user:token:create`](/commands/user/user-token-create) + - **Token Commands** + - [`token:create`](/commands/token/token-create) + - [`token:revoke`](/commands/token/token-revoke) - **Backup Commands** - [`backup:list`](/commands/backup/backup-list) - [`backup:fetch`](/commands/backup/backup-fetch) diff --git a/docs/commands/assets/user-token-create-help.html b/docs/commands/assets/token-create-help.html old mode 100755 new mode 100644 similarity index 98% rename from docs/commands/assets/user-token-create-help.html rename to docs/commands/assets/token-create-help.html index 45751cc9..0cc2b84b --- a/docs/commands/assets/user-token-create-help.html +++ b/docs/commands/assets/token-create-help.html @@ -42,7 +42,7 @@ Creates a new token for an user. Usage: - user:token:create <identifier> <password> + token:create <identifier> <password> Arguments: identifier Unique identifier of the user (email) or the user's UUID diff --git a/docs/commands/assets/user-token-create.html b/docs/commands/assets/token-create.html old mode 100755 new mode 100644 similarity index 97% rename from docs/commands/assets/user-token-create.html rename to docs/commands/assets/token-create.html index 57b5ab30..e0b6658e --- a/docs/commands/assets/user-token-create.html +++ b/docs/commands/assets/token-create.html @@ -43,7 +43,7 @@ ▀ ▀ Ember Nexus API v0.0.37, production mode - User Token Create + Token Create Found user with identifier 'command-test@localhost.dev', user's UUID is 3ae8f319-f585-40bf-8bdf-3d3f990e62e6. diff --git a/docs/commands/assets/token-revoke-help.html b/docs/commands/assets/token-revoke-help.html new file mode 100644 index 00000000..1e5cd1fa --- /dev/null +++ b/docs/commands/assets/token-revoke-help.html @@ -0,0 +1,62 @@ + + + + + + +stdin + + + + +
+Description:
+  Creates a new token for an user.
+
+Usage:
+  token:create <identifier> <password>
+
+Arguments:
+  identifier            Unique identifier of the user (email) or the user's UUID
+  password              Password of the user
+
+Options:
+  -h, --help            Display help for the given command. When no command is given display help for the list command
+  -q, --quiet           Do not output any message
+  -V, --version         Display this application version
+      --ansi|--no-ansi  Force (or disable --no-ansi) ANSI output
+  -n, --no-interaction  Do not ask any interactive question
+  -e, --env=ENV         The Environment name. [default: "prod"]
+      --no-debug        Switch off debug mode.
+  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
+
+ + diff --git a/docs/commands/assets/token-revoke.html b/docs/commands/assets/token-revoke.html new file mode 100644 index 00000000..734e7010 --- /dev/null +++ b/docs/commands/assets/token-revoke.html @@ -0,0 +1,54 @@ + + + + + + +stdin + + + + +
+
+       
+  ▀ ▀  Ember Nexus API
+      v0.0.36, prod mode
+
+  Token Create
+
+  Found user with identifier 'command-test@localhost.dev', user's UUID is a0c8fe7b-f5be-4c08-9dff-44582081ea7c.
+
+▶ Successfully created new token: secret-token:TBOdmNurTLRk0WNrOi0Qdt .
+
+
+ + diff --git a/docs/commands/token/token-create.md b/docs/commands/token/token-create.md new file mode 100644 index 00000000..9706c8c8 --- /dev/null +++ b/docs/commands/token/token-create.md @@ -0,0 +1,17 @@ +# `token:create` + +
Help Output
+ +```bash +php bin/console token:create --help +``` + +[](../assets/token-create-help.html ':include :type=html') + +
Example Command
+ +```bash +php bin/console token:create command-test@localhost.dev 1234 +``` + +[](../assets/token-create.html ':include :type=html') diff --git a/docs/commands/token/token-revoke.md b/docs/commands/token/token-revoke.md new file mode 100644 index 00000000..b9edd4de --- /dev/null +++ b/docs/commands/token/token-revoke.md @@ -0,0 +1,17 @@ +# `token:revoke` + +
Help Output
+ +```bash +php bin/console token:revoke --help +``` + +[](../assets/token-revoke-help.html ':include :type=html') + +
Example Command
+ +```bash +php bin/console token:revoke +``` + +[](../assets/token-revoke.html ':include :type=html') diff --git a/docs/commands/user/user-token-create.md b/docs/commands/user/user-token-create.md deleted file mode 100644 index 244a2240..00000000 --- a/docs/commands/user/user-token-create.md +++ /dev/null @@ -1,17 +0,0 @@ -# `user:token:create` - -
Help Output
- -```bash -php bin/console user:token:create --help -``` - -[](../assets/user-token-create-help.html ':include :type=html') - -
Example Command
- -```bash -php bin/console user:token:create command-test@localhost.dev 1234 -``` - -[](../assets/user-token-create.html ':include :type=html') diff --git a/lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php b/lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php index 8aeccb5f..53b98d63 100644 --- a/lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php +++ b/lib/EmberNexusBundle/src/Service/EmberNexusConfiguration.php @@ -241,6 +241,9 @@ public function setRegisterUniqueIdentifier(string $registerUniqueIdentifier): E if (0 === strlen($registerUniqueIdentifier)) { throw new Exception('Unique identifier can not be an empty string.'); } + if (!preg_match("/^[A-Za-z0-9_]+$/", $registerUniqueIdentifier)) { + throw new Exception("Unique identifier must only contain alphanumeric characters and '_'."); + } $this->registerUniqueIdentifier = $registerUniqueIdentifier; return $this; diff --git a/src/Command/UserTokenCreateCommand.php b/src/Command/TokenCreateCommand.php old mode 100755 new mode 100644 similarity index 97% rename from src/Command/UserTokenCreateCommand.php rename to src/Command/TokenCreateCommand.php index f2efbaf3..ebdf42fa --- a/src/Command/UserTokenCreateCommand.php +++ b/src/Command/TokenCreateCommand.php @@ -22,8 +22,8 @@ /** * @psalm-suppress PropertyNotSetInConstructor $io */ -#[AsCommand(name: 'user:token:create', description: 'Creates a new token for an user.')] -class UserTokenCreateCommand extends Command +#[AsCommand(name: 'token:create', description: 'Creates a new token for an user.')] +class TokenCreateCommand extends Command { private OutputStyle $io; diff --git a/src/Command/TokenRevokeCommand.php b/src/Command/TokenRevokeCommand.php new file mode 100644 index 00000000..c5c0695f --- /dev/null +++ b/src/Command/TokenRevokeCommand.php @@ -0,0 +1,318 @@ +addOption( + self::OPTION_FORCE, + 'f', + InputOption::VALUE_NEGATABLE, + 'If enabled, command will not ask for manual confirmation.', + false + ); + $this->addOption( + self::OPTION_DRY_RUN, + null, + InputOption::VALUE_NEGATABLE, + 'Lists tokens which would be affected by revocation. Does not apply said revocation.', + false + ); + // command filters + $this->addOption( + self::OPTION_USER, + 'u', + InputOption::VALUE_REQUIRED, + 'Token revocation is only applied to given user. User can be specified by its UUID (takes precedent) or its identifier.', + null + ); + $this->addOption( + self::OPTION_GROUP, + 'g', + InputOption::VALUE_REQUIRED, + 'Token revocation is only applied to users of given group. Group must be specified by its UUID.', + null + ); + $this->addOption( + self::OPTION_ISSUED_BEFORE, + 'b', + InputOption::VALUE_REQUIRED, + 'Token revocation is only applied to tokens issued before a given datetime. Datetime must be in the format "YYYY-MM-DD HH:MM" (UTC).', + null + ); + $this->addOption( + self::OPTION_ISSUED_AFTER, + 'a', + InputOption::VALUE_REQUIRED, + 'Token revocation is only applied to tokens issued after a given datetime. Datetime must be in the format "YYYY-MM-DD HH:MM" (UTC).', + null + ); + $this->addOption( + self::OPTION_ISSUED_WITHOUT_EXPIRATION_DATE, + null, + InputOption::VALUE_NEGATABLE, + 'Token revocation is only applied to tokens with no explicit expiration date.', + false + ); + $this->addOption( + self::OPTION_ISSUED_WITH_EXPIRATION_DATE, + null, + InputOption::VALUE_NEGATABLE, + 'Token revocation is only applied to tokens with explicit expiration date set.', + false + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->io = new EmberNexusStyle($input, $output); + + $this->io->title('Token Revocation'); + + $filters = []; + $arguments = []; + + if ( + $input->getOption(self::OPTION_ISSUED_WITH_EXPIRATION_DATE) === true && + $input->getOption(self::OPTION_ISSUED_WITHOUT_EXPIRATION_DATE) === true + ) { + throw new Exception(sprintf( + "Using both %s and %s is not possible.", + self::OPTION_ISSUED_WITH_EXPIRATION_DATE, + self::OPTION_ISSUED_WITHOUT_EXPIRATION_DATE + )); + } + if ($input->getOption(self::OPTION_ISSUED_WITH_EXPIRATION_DATE) === true) { + $filters[] = 't.expirationDate IS NOT NULL'; + } + if ($input->getOption(self::OPTION_ISSUED_WITHOUT_EXPIRATION_DATE) === true) { + $filters[] = 't.expirationDate IS NULL'; + } + + + $optionIssuedBefore = null; + if ($input->getOption(self::OPTION_ISSUED_BEFORE) !== null) { + $optionIssuedBefore = DateTime::createFromFormat('Y-m-d H:i', $input->getOption(self::OPTION_ISSUED_BEFORE)); + $filters[] = 't.created < $issuedBefore'; + $arguments['issuedBefore'] = $optionIssuedBefore; + } + + $optionIssuedAfter = null; + if ($input->getOption(self::OPTION_ISSUED_AFTER) !== null) { + $optionIssuedAfter = DateTime::createFromFormat('Y-m-d H:i', $input->getOption(self::OPTION_ISSUED_AFTER)); + $filters[] = 't.created > $issuedAfter'; + $arguments['issuedAfter'] = $optionIssuedAfter; + } + + if ($optionIssuedBefore && $optionIssuedAfter) { + if ($optionIssuedBefore->getTimestamp() < $optionIssuedAfter->getTimestamp()) { + throw new Exception(sprintf( + "%s can not be before %s.", + self::OPTION_ISSUED_BEFORE, + self::OPTION_ISSUED_AFTER + )); + } + } + + if ($input->getOption(self::OPTION_USER) !== null) { + $userIdentifier = $input->getOption(self::OPTION_USER); + if (preg_match(Regex::UUID_V4, $userIdentifier)) { + $filters[] = 'u.id = $userIdentifier'; + } else { + $filters[] = sprintf( + "u.%s = \$userIdentifier", + $this->emberNexusConfiguration->getRegisterUniqueIdentifier() + ); + } + $arguments['userIdentifier'] = $userIdentifier; + } + + if ($input->getOption(self::OPTION_GROUP) !== null) { + $filters[] = '(t)<-[:OWNS]-(:User)-[:IS_IN_GROUP*1..]->(:Group {id: $groupIdentifier})'; + $arguments['groupIdentifier'] = $input->getOption(self::OPTION_GROUP); + } + + $joinedFilters = join("\nAND ", $filters); + $finalQuery = sprintf( + "MATCH (t:Token)<-[:OWNS]-(u:User)\n". + "%s%s%s". + "RETURN t.id, t.created, t.expirationDate, u.id, u.%s as userUniqueIdentifier\n". + "ORDER BY t.created ASC, t.id ASC", + count($filters) > 0 ? 'WHERE ' : '', + $joinedFilters, + count($filters) > 0 ? "\n" : '', + $this->emberNexusConfiguration->getRegisterUniqueIdentifier() + ); + +// $this->io->writeln("----------------------------"); +// +// foreach ($filters as $filter) { +// $this->io->writeln(' '.$filter); +// } +// +// $this->io->writeln("----------------------------"); + + $res = $this->cypherEntityManager->getClient()->runStatement( + new Statement($finalQuery, $arguments) + ); + + + $countTokensToBeRevoked = count($res); + if ($countTokensToBeRevoked === 0) { + $this->io->finalMessage('No tokens found.'); + return Command::SUCCESS; + } + + $this->io->writeln(sprintf( + " %d tokens are affected of revocation%s", + $countTokensToBeRevoked, + $countTokensToBeRevoked > 10 ? '. First 10 are shown:' : ':' + )); + $this->io->newLine(); + + $rows = []; + foreach ($res as $i => $tokenResult) { + $tokenCreated = $tokenResult['t.created']; + if ($tokenCreated instanceof DateTimeZoneId) { + $tokenCreated = $tokenCreated->toDateTime()->format('Y-m-d H:i:s'); + } + $tokenExpires = $tokenResult['t.expirationDate']; + if ($tokenExpires instanceof DateTimeZoneId) { + $tokenExpires = $tokenExpires->toDateTime()->format('Y-m-d H:i:s'); + } + $rows[] = [ + $tokenResult['t.id'], + $tokenResult['u.id'], + $tokenResult['userUniqueIdentifier'], + $tokenCreated, + $tokenExpires ?? '-' + ]; + if (count($rows) >= 10) { + break; + } + } + + $table = $this->io->createCompactTable(); + $table->setHeaders([ + 'Token UUID', + 'User UUID', + 'User identifier', + 'Created', + 'Expires on' + ]); + $table->setRows($rows); + $table->render(); + $this->io->newLine(); + + + /** + * @var QuestionHelper $helper + */ + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion(sprintf( + " Are you sure you want to revoke all %d tokens? [y/N]: ", + $countTokensToBeRevoked + ), false); + if (!$helper->ask($input, $output, $question)) { + $this->io->finalMessage('Aborted revoking tokens.'); + + return self::FAILURE; + } + $this->io->newLine(); + +// foreach ($res as $tokenResult) { +// $this->io->writeln(sprintf( +// "Token: %s, owned by %s", +// $tokenResult['t.id'], +// $tokenResult['userUniqueIdentifier'] +// )); +// } + + + + + + + + + +// $normalizedArguments = []; +// foreach ($arguments as $i => $argument) { +// if ($argument instanceof \DateTimeInterface) { +// $argument = 'datetime("'.$argument->format('c').'")'; +// } else { +// $argument = \Safe\json_encode($argument, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); +// } +// $normalizedArguments[] = sprintf( +// " %s: %s,", +// $i, +// $argument +// ); +// } +// +// $finalArguments = sprintf( +// ":params\n{\n%s\n}", +// join("\n", $normalizedArguments) +// ); +// +// $this->io->writeln($finalArguments); +// $this->io->newLine(); +// $this->io->writeln($finalQuery); + + $this->io->finalMessage('Successfully revoked tokens.'); + + return Command::SUCCESS; + } +} diff --git a/src/Security/TokenGenerator.php b/src/Security/TokenGenerator.php index b04d8516..7e618c1d 100755 --- a/src/Security/TokenGenerator.php +++ b/src/Security/TokenGenerator.php @@ -71,6 +71,7 @@ public function createNewToken(UuidInterface $userUuid, array $data = [], int $l ->addProperties([ 'hash' => $hash, 'expirationDate' => (new DateTime())->add(new DateInterval(sprintf('PT%sS', $lifetimeInSeconds))), + 'state' => '' ]); $this->elementManager->create($tokenNode); $this->elementManager->flush(); diff --git a/src/Type/TokenStateType.php b/src/Type/TokenStateType.php new file mode 100644 index 00000000..2270f998 --- /dev/null +++ b/src/Type/TokenStateType.php @@ -0,0 +1,17 @@ +