Passed
Push — master ( 03be53...8cd3b2 )
by BENOIT
02:26
created

GenerateJWTCommand::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 4
rs 10
1
<?php
2
3
namespace BenTools\MercurePHP\Command;
4
5
use BenTools\MercurePHP\Configuration\Configuration;
6
use Lcobucci\JWT\Builder;
7
use Lcobucci\JWT\Signer\Key;
8
use Symfony\Component\Console\Command\Command;
9
use Symfony\Component\Console\Input\InputInterface;
10
use Symfony\Component\Console\Input\InputOption;
11
use Symfony\Component\Console\Output\OutputInterface;
12
use Symfony\Component\Console\Style\SymfonyStyle;
13
14
use function BenTools\MercurePHP\get_signer;
15
use function BenTools\MercurePHP\without_nullish_values;
16
17
final class GenerateJWTCommand extends Command
18
{
19
    private const TARGET_PUBLISHERS = 'publishers';
20
    private const TARGET_SUBSCRIBERS = 'subscribers';
21
    private const TARGET_BOTH = 'both';
22
    private const VALID_TARGETS = [
23
        self::TARGET_PUBLISHERS,
24
        self::TARGET_SUBSCRIBERS,
25
        self::TARGET_BOTH,
26
    ];
27
28
    protected static $defaultName = 'mercure:jwt:generate';
29
30
    private Configuration $configuration;
31
32
    public function __construct(Configuration $configuration)
33
    {
34
        parent::__construct();
35
        $this->configuration = $configuration;
36
    }
37
38
    protected function execute(InputInterface $input, OutputInterface $output): int
39
    {
40
        $io = new SymfonyStyle($input, $output);
41
        $config = $this->configuration->overrideWith(without_nullish_values($input->getOptions()))->asArray();
42
43
        $target = $input->getOption('target') ?? self::TARGET_BOTH;
44
        if (!\in_array($target, self::VALID_TARGETS, true)) {
45
            $io->error(\sprintf('Invalid target `%s`.', $target));
0 ignored issues
show
Bug introduced by
It seems like $target can also be of type string[]; however, parameter $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

45
            $io->error(\sprintf('Invalid target `%s`.', /** @scrutinizer ignore-type */ $target));
Loading history...
46
47
            return self::FAILURE;
48
        }
49
50
        $values = [
51
            'publish' => $input->getOption('publish'),
52
            'publish_exclude' => $input->getOption('publish-exclude'),
53
            'subscribe' => $input->getOption('subscribe'),
54
            'subscribe_exclude' => $input->getOption('subscribe-exclude'),
55
        ];
56
57
        $claim = [];
58
59
        if (\in_array($target, [self::TARGET_PUBLISHERS, self::TARGET_BOTH], true)) {
60
            $claim = [
61
                'publish' => $values['publish'],
62
                'publish_exclude' => $values['publish_exclude'],
63
            ];
64
        }
65
66
        if (\in_array($target, [self::TARGET_SUBSCRIBERS, self::TARGET_BOTH], true)) {
67
            $claim = \array_merge(
0 ignored issues
show
Unused Code introduced by
The assignment to $claim is dead and can be removed.
Loading history...
68
                $claim,
69
                [
70
                    'subscribe' => $values['subscribe'],
71
                    'subscribe_exclude' => $values['subscribe_exclude'],
72
                ]
73
            );
74
        }
75
76
        $claim = \array_filter($values, fn(array $claim) => [] !== $claim);
77
        $containsPublishTopics = isset($claim['publish']) || isset($claim['publish_exclude']);
78
        $containsSubscribeTopics = isset($claim['subscribe']) || isset($claim['subscribe_exclude']);
79
        $builder = (new Builder())->withClaim('mercure', $claim);
80
81
        if (null !== $input->getOption('ttl')) {
82
            $builder = $builder->expiresAt(\time() + (int) $input->getOption('ttl'));
83
        }
84
85
        $defaultKey = $config[Configuration::JWT_KEY];
86
        $defaultAlgorithm = $config[Configuration::JWT_ALGORITHM];
87
88
        if (isset($config[Configuration::PUBLISHER_JWT_KEY])) {
89
            $publisherKey = $config[Configuration::PUBLISHER_JWT_KEY];
90
            $publisherAlgorithm = $config[Configuration::PUBLISHER_JWT_ALGORITHM] ?? $config[Configuration::JWT_ALGORITHM];
91
        }
92
93
        if (isset($config[Configuration::SUBSCRIBER_JWT_KEY])) {
94
            $subscriberKey = $config[Configuration::SUBSCRIBER_JWT_KEY];
95
            $subscriberAlgorithm = $config[Configuration::SUBSCRIBER_JWT_ALGORITHM] ?? $config[Configuration::JWT_ALGORITHM];
96
        }
97
98
        if (true === $containsPublishTopics && false === $containsSubscribeTopics) {
99
            $target = self::TARGET_PUBLISHERS;
100
        } elseif (false === $containsPublishTopics && true === $containsSubscribeTopics) {
101
            $target = self::TARGET_SUBSCRIBERS;
102
        }
103
104
        switch ($target) {
105
            case self::TARGET_PUBLISHERS:
106
                $key = $publisherKey ?? $defaultKey;
107
                $algorithm = $publisherAlgorithm ?? $defaultAlgorithm;
108
                break;
109
            case self::TARGET_SUBSCRIBERS:
110
                $key = $subscriberKey ?? $defaultKey;
111
                $algorithm = $subscriberAlgorithm ?? $defaultAlgorithm;
112
                break;
113
            case self::TARGET_BOTH:
114
            default:
115
                $key = $defaultKey;
116
                $algorithm = $defaultAlgorithm;
117
        }
118
119
        try {
120
            $token = $builder->getToken(
121
                get_signer($algorithm),
122
                new Key($key),
123
            );
124
        } catch (\Exception $e) {
125
            $io->error('Unable to sign your token.');
126
127
            return self::FAILURE;
128
        }
129
130
        if (false === $input->getOption('raw')) {
131
            $io->success('Here is your token! ⤵️');
132
        }
133
        $output->writeln((string) $token);
134
135
        return self::SUCCESS;
136
    }
137
138
    protected function interact(InputInterface $input, OutputInterface $output): void
139
    {
140
        $io = new SymfonyStyle($input, $output);
141
        $config = Configuration::bootstrapFromCLI($input)->asArray();
142
        $publishersOnly = !empty($config[Configuration::PUBLISHER_JWT_KEY]);
143
        $subscribersOnly = !empty($config[Configuration::SUBSCRIBER_JWT_KEY]);
144
        $forBothTargets = false === $publishersOnly && false === $subscribersOnly;
145
146
        if (!$forBothTargets && empty($input->getOption('target'))) {
147
            $value = $io->choice(
148
                'Do you want to generate a JWT for publishers or subscribers?',
149
                [
150
                    self::TARGET_PUBLISHERS,
151
                    self::TARGET_SUBSCRIBERS,
152
                ]
153
            );
154
155
            $input->setOption('target', $value);
156
        }
157
158
        if ($forBothTargets || self::TARGET_PUBLISHERS === $input->getOption('target')) {
159
            $values = (array) $input->getOption('publish');
160
            if (empty($values)) {
161
                ASK_PUBLISH:
162
                $value = $io->ask(
163
                    'Add a topic selector for the `publish` key (or just hit ENTER when you\'re done)'
164
                );
165
                if (null !== $value) {
166
                    $values[] = $value;
167
                    goto ASK_PUBLISH;
168
                }
169
                $input->setOption('publish', $values);
170
            }
171
172
            $values = (array) $input->getOption('publish-exclude');
173
            if (empty($values)) {
174
                ASK_PUBLISH_EXCLUDE:
175
                $value = $io->ask(
176
                    'Add a topic selector for the `publish-exclude` key (or just hit ENTER when you\'re done)'
177
                );
178
                if (null !== $value) {
179
                    $values[] = $value;
180
                    goto ASK_PUBLISH_EXCLUDE;
181
                }
182
                $input->setOption('publish-exclude', $values);
183
            }
184
        }
185
186
        if ($forBothTargets || self::TARGET_SUBSCRIBERS === $input->getOption('target')) {
187
            $values = (array) $input->getOption('subscribe');
188
            if (empty($values)) {
189
                ASK_SUBSCRIBE:
190
                $value = $io->ask(
191
                    'Add a topic selector for the `subscribe` key (or just hit ENTER when you\'re done)'
192
                );
193
                if (null !== $value) {
194
                    $values[] = $value;
195
                    goto ASK_SUBSCRIBE;
196
                }
197
                $input->setOption('subscribe', $values);
198
            }
199
200
            $values = (array) $input->getOption('subscribe-exclude');
201
            if (empty($values)) {
202
                ASK_SUBSCRIBE_EXCLUDE:
203
                $value = $io->ask(
204
                    'Add a topic selector for the `subscribe-exclude` key (or just hit ENTER when you\'re done)'
205
                );
206
                if (null !== $value) {
207
                    $values[] = $value;
208
                    goto ASK_SUBSCRIBE_EXCLUDE;
209
                }
210
                $input->setOption('subscribe-exclude', $values);
211
            }
212
        }
213
214
        if (null === $input->getOption('ttl')) {
215
            $value = $io->ask(
216
                'TTL of this token in seconds (or hit ENTER for no expiration):',
217
                null,
218
                function ($value) {
219
                    if (null === $value) {
220
                        return null;
221
                    }
222
                    if (!\is_numeric($value) || $value <= 0) {
223
                        throw new \RuntimeException('Invalid number.');
224
                    }
225
226
                    return $value;
227
                }
228
            );
229
230
            $input->setOption('ttl', $value);
231
        }
232
    }
233
234
    protected function configure(): void
235
    {
236
        $this->setDescription('Generates a JWT key to use on this hub.');
237
        $this
238
            ->addOption(
239
                'publish',
240
                null,
241
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
242
                'Allowed topic selectors for publishing.',
243
                []
244
            )
245
            ->addOption(
246
                'publish-exclude',
247
                null,
248
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
249
                'Denied topic selectors for publishing.',
250
                []
251
            )
252
            ->addOption(
253
                'subscribe',
254
                null,
255
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
256
                'Allowed topic selectors for subscribing.',
257
                []
258
            )
259
            ->addOption(
260
                'subscribe-exclude',
261
                null,
262
                InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
263
                'Denied topic selectors for subscribing.',
264
                []
265
            )
266
            ->addOption(
267
                'target',
268
                null,
269
                InputOption::VALUE_OPTIONAL
270
            )
271
            ->addOption(
272
                'ttl',
273
                null,
274
                InputOption::VALUE_OPTIONAL,
275
                'TTL of this token, in seconds.'
276
            )
277
            ->addOption(
278
                'raw',
279
                null,
280
                InputOption::VALUE_NONE,
281
                'Enable raw output'
282
            )
283
            ->addOption(
284
                'jwt-key',
285
                null,
286
                InputOption::VALUE_OPTIONAL,
287
                'The JWT key to use for both publishers and subscribers',
288
            )
289
            ->addOption(
290
                'jwt-algorithm',
291
                null,
292
                InputOption::VALUE_OPTIONAL,
293
                'The JWT verification algorithm to use for both publishers and subscribers, e.g. HS256 (default) or RS512.',
294
            )
295
            ->addOption(
296
                'publisher-jwt-key',
297
                null,
298
                InputOption::VALUE_OPTIONAL,
299
                'Must contain the secret key to valid publishers\' JWT, can be omitted if jwt_key is set.',
300
            )
301
            ->addOption(
302
                'publisher-jwt-algorithm',
303
                null,
304
                InputOption::VALUE_OPTIONAL,
305
                'The JWT verification algorithm to use for publishers, e.g. HS256 (default) or RS512.',
306
            )
307
            ->addOption(
308
                'subscriber-jwt-key',
309
                null,
310
                InputOption::VALUE_OPTIONAL,
311
                'Must contain the secret key to valid subscribers\' JWT, can be omitted if jwt_key is set.',
312
            )
313
            ->addOption(
314
                'subscriber-jwt-algorithm',
315
                null,
316
                InputOption::VALUE_OPTIONAL,
317
                'The JWT verification algorithm to use for subscribers, e.g. HS256 (default) or RS512.',
318
            );
319
    }
320
}
321