From 30e56ade855f04af8922338193f50d65bea3b617 Mon Sep 17 00:00:00 2001 From: Pepa Martinec Date: Tue, 23 Jun 2026 21:20:03 +0200 Subject: [PATCH 1/9] feat(api-bundle): add StorageClientApiFactory building client from resolved StorageApiToken --- .../StorageClientApiFactory.php | 63 +++++++++ .../StorageClientApiFactoryTest.php | 121 ++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 libs/api-bundle/src/StorageApiClient/StorageClientApiFactory.php create mode 100644 libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryTest.php diff --git a/libs/api-bundle/src/StorageApiClient/StorageClientApiFactory.php b/libs/api-bundle/src/StorageApiClient/StorageClientApiFactory.php new file mode 100644 index 000000000..8f433ef83 --- /dev/null +++ b/libs/api-bundle/src/StorageApiClient/StorageClientApiFactory.php @@ -0,0 +1,63 @@ +clientOptions = new ClientOptions(); + $this->clientOptions->addValuesFrom($clientOptions); + } + + public function getClientOptionsReadOnly(): ClientOptions + { + return clone $this->clientOptions; + } + + public function createClientWrapper( + Request $request, + StorageApiToken $storageToken, + ?ClientOptions $clientOptions = null, + ): ClientWrapper { + $options = clone $this->clientOptions; + if ($clientOptions) { + $options->addValuesFrom($clientOptions); + } + + $options->setToken($storageToken->getTokenValue()); + $options->setAuthType(AuthType::STORAGE_TOKEN); + $options->setRunId($this->getRunId($request, $options)); + + return new ClientWrapper($options); + } + + private function getRunId(Request $request, ClientOptions $options): string + { + $runId = (string) $request->headers->get(self::RUN_ID_HEADER); + + if ($runId === '') { + $runIdGenerator = $options->getRunIdGenerator(); + if ($runIdGenerator !== null) { + $runId = $runIdGenerator($options); + assert(is_string($runId)); + } else { + $runId = uniqid('run-'); + } + } + return $runId; + } +} diff --git a/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryTest.php b/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryTest.php new file mode 100644 index 000000000..49702536f --- /dev/null +++ b/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryTest.php @@ -0,0 +1,121 @@ + 'header-storage-token', + self::AUTHORIZATION_HEADER => 'Bearer kbc_pat_from_header', + ]); + $factory = new StorageClientApiFactory(new ClientOptions('https://connection.test')); + + $wrapper = $factory->createClientWrapper($request, self::storageApiToken('resolved-storage-token')); + + self::assertSame('resolved-storage-token', $wrapper->getClientOptionsReadOnly()->getToken()); + self::assertSame(AuthType::STORAGE_TOKEN, $wrapper->getClientOptionsReadOnly()->getAuthType()); + } + + public function testRunIdTakenFromHeaderWhenPresent(): void + { + $request = new Request([], [], [], [], [], [self::RUN_ID_HEADER => '123']); + $factory = new StorageClientApiFactory(new ClientOptions('https://connection.test')); + + $wrapper = $factory->createClientWrapper($request, self::storageApiToken('t')); + + self::assertSame('123', $wrapper->getClientOptionsReadOnly()->getRunId()); + } + + public function testRunIdFallsBackToGeneratedValueWhenHeaderMissing(): void + { + $request = new Request(); + $factory = new StorageClientApiFactory(new ClientOptions('https://connection.test')); + + $wrapper = $factory->createClientWrapper($request, self::storageApiToken('t')); + + self::assertStringStartsWith('run-', (string) $wrapper->getClientOptionsReadOnly()->getRunId()); + } + + public function testRunIdGeneratorUsedWhenHeaderMissing(): void + { + $request = new Request(); + $options = new ClientOptions('https://connection.test'); + $options->setRunIdGenerator(fn (ClientOptions $o): string => 'gen-' . $o->getUrl()); + $factory = new StorageClientApiFactory($options); + + $wrapper = $factory->createClientWrapper($request, self::storageApiToken('t')); + + self::assertSame('gen-https://connection.test', $wrapper->getClientOptionsReadOnly()->getRunId()); + } + + public function testPerCallClientOptionsAreMergedOverBase(): void + { + $request = new Request(); + $factory = new StorageClientApiFactory(new ClientOptions('https://connection.test')); + + $wrapper = $factory->createClientWrapper( + $request, + self::storageApiToken('t'), + new ClientOptions(branchId: '1234'), + ); + + self::assertSame('1234', $wrapper->getClientOptionsReadOnly()->getBranchId()); + self::assertSame('https://connection.test', $wrapper->getClientOptionsReadOnly()->getUrl()); + } + + public function testBaseOptionsAreNotMutated(): void + { + $request = new Request(); + $base = new ClientOptions('https://connection.test'); + $factory = new StorageClientApiFactory($base); + + $factory->createClientWrapper($request, self::storageApiToken('t')); + + // The token must not leak back into the caller's base options nor the factory's own copy. + self::assertNull($base->getToken()); + self::assertNull($factory->getClientOptionsReadOnly()->getToken()); + } + + public function testGetClientOptionsReadOnlyReturnsIsolatedClone(): void + { + $factory = new StorageClientApiFactory(new ClientOptions('https://foo')); + + self::assertSame('https://foo', $factory->getClientOptionsReadOnly()->getUrl()); + $factory->getClientOptionsReadOnly()->setUrl('https://bar'); + self::assertSame('https://foo', $factory->getClientOptionsReadOnly()->getUrl()); + } + + public function testAcceptsSecurityStorageApiTokenSubclass(): void + { + $request = new Request(); + $factory = new StorageClientApiFactory(new ClientOptions('https://connection.test')); + $token = new SecurityStorageApiToken([], 'subclass-token'); + + $wrapper = $factory->createClientWrapper($request, $token); + + self::assertSame('subclass-token', $wrapper->getClientOptionsReadOnly()->getToken()); + } +} From 1cff8b7714e801190500841c26f6f5f553ba4ead Mon Sep 17 00:00:00 2001 From: Pepa Martinec Date: Tue, 23 Jun 2026 22:04:02 +0200 Subject: [PATCH 2/9] feat(api-bundle): register StorageClientApiFactory as a service with base options --- libs/api-bundle/README.md | 30 ++++++++++++++++ .../KeboolaApiExtension.php | 17 +++++++++ .../KeboolaApiExtensionTest.php | 35 +++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/libs/api-bundle/README.md b/libs/api-bundle/README.md index f32746fd1..b3ed6fc29 100644 --- a/libs/api-bundle/README.md +++ b/libs/api-bundle/README.md @@ -145,6 +145,36 @@ which the bundle requires directly, so no extra installation is needed. > If you forget to install appropriate client, you will get exception like > `Service "Keboola\ApiBundle\Attribute\ApplicationTokenAuth" not found: the container inside "Symfony\Component\DependencyInjection\Argument\ServiceLocator" is a smaller service locator` +## Storage API client + +When `#[StorageApiTokenAuth]` is enabled, the bundle registers a +`Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory` service. Depend on it and build a +Storage `ClientWrapper` from the resolved `#[CurrentUser] StorageApiToken` — unlike the header-based +`StorageClientRequestFactory`, it uses the token already resolved by the authenticator, so it works +for programmatic (`kbc_pat_*` / `kbc_at_*`) tokens too. + +The base options are preconfigured by the bundle: the Connection (Storage API) URL is taken from the +`ServiceClient`, the logger is the shared logger, and the user agent is the configured `app_name`. +The token comes from the `StorageApiToken` you pass, the run id from the request's `X-KBC-RunId` +header, and branch / backend from an optional per-call `ClientOptions`. + +```php +public function __construct( + private readonly StorageClientApiFactory $storageClientApiFactory, +) {} + +#[StorageApiTokenAuth] +public function __invoke(#[CurrentUser] StorageApiToken $token, Request $request) +{ + $client = $this->storageClientApiFactory->createClientWrapper($request, $token)->getBasicClient(); + + // branch-aware / per-call overrides: + // $wrapper = $this->storageClientApiFactory->createClientWrapper( + // $request, $token, new ClientOptions(branchId: $branchId), + // ); +} +``` + ## Testing controllers `Keboola\ApiBundle\Test\AuthenticatorTestTrait` stubs the authenticators in functional diff --git a/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php b/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php index 2f4cb8c93..608f8480b 100644 --- a/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php +++ b/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php @@ -10,12 +10,15 @@ use Keboola\ApiBundle\Security\ApplicationToken\ManageApiClientFactory; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenAuthenticator; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenFactory; +use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory; use Keboola\ManageApi\Client as ManageApiClient; use Keboola\ServiceClient\ServiceClient; use Keboola\ServiceClient\ServiceDnsType; +use Keboola\StorageApiBranch\Factory\ClientOptions; use Keboola\StorageApiBranch\Factory\StorageClientRequestFactory; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; @@ -92,6 +95,20 @@ private function setupStorageApiAuthenticator( ->setArgument('$tokenFactory', new Reference(StorageApiTokenFactory::class)) ; $authenticators[StorageApiTokenAuth::class] = new Reference(StorageApiTokenAuthenticator::class); + + // Storage API client factory available to controllers/services for building a ClientWrapper + // from the resolved StorageApiToken. + $connectionUrl = (new Definition()) + ->setFactory([new Reference(ServiceClient::class), 'getConnectionServiceUrl']); + + $baseClientOptions = (new Definition(ClientOptions::class)) + ->setArgument('$url', $connectionUrl) + ->setArgument('$logger', new Reference('logger')) + ->setArgument('$userAgent', $config['app_name']); + + $container->register(StorageClientApiFactory::class) + ->setArgument('$clientOptions', $baseClientOptions) + ; } private function setupApplicationTokenAuthenticator( diff --git a/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php b/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php index 7c2c38e5f..d35bf45fc 100644 --- a/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php +++ b/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php @@ -8,9 +8,13 @@ use Keboola\ApiBundle\Security\ApplicationToken\ManageApiClientFactory; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenAuthenticator; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenFactory; +use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory; use Keboola\ManageApi\Client as ManageApiClient; +use Keboola\ServiceClient\ServiceClient; +use Keboola\StorageApiBranch\Factory\ClientOptions; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; class KeboolaApiExtensionTest extends TestCase @@ -52,6 +56,37 @@ public function testStorageApiServicesAreRegistered(): void ); } + public function testStorageClientApiFactoryIsRegisteredWithBaseOptions(): void + { + $container = $this->buildContainer([['app_name' => 'storage-test-app']]); + + self::assertTrue( + $container->hasDefinition(StorageClientApiFactory::class), + 'StorageClientApiFactory must be registered', + ); + + $clientOptions = $container->getDefinition(StorageClientApiFactory::class)->getArgument('$clientOptions'); + self::assertInstanceOf(Definition::class, $clientOptions); + self::assertSame(ClientOptions::class, $clientOptions->getClass()); + + // userAgent is the configured app name + self::assertSame('storage-test-app', $clientOptions->getArgument('$userAgent')); + + // logger is the shared @logger service + $logger = $clientOptions->getArgument('$logger'); + self::assertInstanceOf(Reference::class, $logger); + self::assertSame('logger', (string) $logger); + + // url is resolved at runtime from ServiceClient::getConnectionServiceUrl() + $url = $clientOptions->getArgument('$url'); + self::assertInstanceOf(Definition::class, $url); + $urlFactory = $url->getFactory(); + self::assertIsArray($urlFactory); + self::assertInstanceOf(Reference::class, $urlFactory[0]); + self::assertSame(ServiceClient::class, (string) $urlFactory[0]); + self::assertSame('getConnectionServiceUrl', $urlFactory[1]); + } + // ------------------------------------------------------------------------- // Resolver client wiring // ------------------------------------------------------------------------- From aa5af0a68edeb960056ae6bf91bf528c83e87885 Mon Sep 17 00:00:00 2001 From: Pepa Martinec Date: Wed, 24 Jun 2026 08:29:02 +0200 Subject: [PATCH 3/9] feat(api-bundle): add request-bound RequestStorageClientFactory with controller value resolver --- libs/api-bundle/README.md | 16 +++ .../KeboolaApiExtension.php | 11 +++ .../RequestStorageClientFactory.php | 25 +++++ .../StorageApiClientResolver.php | 39 ++++++++ .../KeboolaApiExtensionTest.php | 23 +++++ .../RequestStorageClientFactoryTest.php | 41 ++++++++ .../StorageApiClientResolverTest.php | 97 +++++++++++++++++++ libs/api-bundle/tests/config.yaml | 8 ++ 8 files changed, 260 insertions(+) create mode 100644 libs/api-bundle/src/StorageApiClient/RequestStorageClientFactory.php create mode 100644 libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php create mode 100644 libs/api-bundle/tests/StorageApiClient/RequestStorageClientFactoryTest.php create mode 100644 libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php diff --git a/libs/api-bundle/README.md b/libs/api-bundle/README.md index b3ed6fc29..97eea7af5 100644 --- a/libs/api-bundle/README.md +++ b/libs/api-bundle/README.md @@ -175,6 +175,22 @@ public function __invoke(#[CurrentUser] StorageApiToken $token, Request $request } ``` +For controllers, you can skip the `$request`/`$token` plumbing: type-hint +`Keboola\ApiBundle\StorageApiClient\RequestStorageClientFactory` on a controller argument and the +bundle injects a factory already bound to the current request and resolved `StorageApiToken`. You +only pass optional per-call `ClientOptions`: + +```php +#[StorageApiTokenAuth] +public function __invoke(RequestStorageClientFactory $storage) +{ + $client = $storage->createClientWrapper()->getBasicClient(); + + // branch-aware / per-call overrides: + // $wrapper = $storage->createClientWrapper(new ClientOptions(branchId: $branchId)); +} +``` + ## Testing controllers `Keboola\ApiBundle\Test\AuthenticatorTestTrait` stubs the authenticators in functional diff --git a/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php b/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php index 608f8480b..e7eb7c968 100644 --- a/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php +++ b/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php @@ -10,6 +10,7 @@ use Keboola\ApiBundle\Security\ApplicationToken\ManageApiClientFactory; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenAuthenticator; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenFactory; +use Keboola\ApiBundle\StorageApiClient\StorageApiClientResolver; use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory; use Keboola\ManageApi\Client as ManageApiClient; use Keboola\ServiceClient\ServiceClient; @@ -22,6 +23,7 @@ use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; class KeboolaApiExtension extends Extension { @@ -109,6 +111,15 @@ private function setupStorageApiAuthenticator( $container->register(StorageClientApiFactory::class) ->setArgument('$clientOptions', $baseClientOptions) ; + + // Controller-argument value resolver that hands controllers a RequestStorageClientFactory + // bound to the current request and the resolved StorageApiToken. Triggers purely on the + // argument type; the token comes from security's TokenStorage (same source as #[CurrentUser]). + $container->register(StorageApiClientResolver::class) + ->setArgument('$factory', new Reference(StorageClientApiFactory::class)) + ->setArgument('$tokenStorage', new Reference(TokenStorageInterface::class)) + ->addTag('controller.argument_value_resolver') + ; } private function setupApplicationTokenAuthenticator( diff --git a/libs/api-bundle/src/StorageApiClient/RequestStorageClientFactory.php b/libs/api-bundle/src/StorageApiClient/RequestStorageClientFactory.php new file mode 100644 index 000000000..ff9ad85c6 --- /dev/null +++ b/libs/api-bundle/src/StorageApiClient/RequestStorageClientFactory.php @@ -0,0 +1,25 @@ +factory->createClientWrapper($this->request, $this->token, $clientOptions); + } +} diff --git a/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php b/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php new file mode 100644 index 000000000..fd8528414 --- /dev/null +++ b/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php @@ -0,0 +1,39 @@ +getType() !== RequestStorageClientFactory::class) { + return []; + } + + $user = $this->tokenStorage->getToken()?->getUser(); + if (!$user instanceof StorageApiToken) { + throw new RuntimeException(sprintf( + 'Cannot resolve argument "$%s": no authenticated Storage API token. ' + . 'The controller must be guarded by #[StorageApiTokenAuth].', + $argument->getName(), + )); + } + + return [new RequestStorageClientFactory($this->factory, $request, $user)]; + } +} diff --git a/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php b/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php index d35bf45fc..0a437bd29 100644 --- a/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php +++ b/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php @@ -8,6 +8,7 @@ use Keboola\ApiBundle\Security\ApplicationToken\ManageApiClientFactory; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenAuthenticator; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenFactory; +use Keboola\ApiBundle\StorageApiClient\StorageApiClientResolver; use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory; use Keboola\ManageApi\Client as ManageApiClient; use Keboola\ServiceClient\ServiceClient; @@ -16,6 +17,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; class KeboolaApiExtensionTest extends TestCase { @@ -87,6 +89,27 @@ public function testStorageClientApiFactoryIsRegisteredWithBaseOptions(): void self::assertSame('getConnectionServiceUrl', $urlFactory[1]); } + public function testStorageApiClientResolverIsRegisteredAndTagged(): void + { + $container = $this->buildContainer([[]]); + + self::assertTrue( + $container->hasDefinition(StorageApiClientResolver::class), + 'StorageApiClientResolver must be registered', + ); + + $definition = $container->getDefinition(StorageApiClientResolver::class); + self::assertArrayHasKey('controller.argument_value_resolver', $definition->getTags()); + + $factory = $definition->getArgument('$factory'); + self::assertInstanceOf(Reference::class, $factory); + self::assertSame(StorageClientApiFactory::class, (string) $factory); + + $tokenStorage = $definition->getArgument('$tokenStorage'); + self::assertInstanceOf(Reference::class, $tokenStorage); + self::assertSame(TokenStorageInterface::class, (string) $tokenStorage); + } + // ------------------------------------------------------------------------- // Resolver client wiring // ------------------------------------------------------------------------- diff --git a/libs/api-bundle/tests/StorageApiClient/RequestStorageClientFactoryTest.php b/libs/api-bundle/tests/StorageApiClient/RequestStorageClientFactoryTest.php new file mode 100644 index 000000000..33e46e221 --- /dev/null +++ b/libs/api-bundle/tests/StorageApiClient/RequestStorageClientFactoryTest.php @@ -0,0 +1,41 @@ + '42']); + $token = new StorageApiToken([], 'bound-token'); + $base = new StorageClientApiFactory(new ClientOptions('https://connection.test')); + + $factory = new RequestStorageClientFactory($base, $request, $token); + $wrapper = $factory->createClientWrapper(); + + self::assertSame('bound-token', $wrapper->getClientOptionsReadOnly()->getToken()); + self::assertSame('42', $wrapper->getClientOptionsReadOnly()->getRunId()); + } + + public function testCreateClientWrapperMergesPerCallOptions(): void + { + $request = new Request(); + $token = new StorageApiToken([], 'bound-token'); + $base = new StorageClientApiFactory(new ClientOptions('https://connection.test')); + + $factory = new RequestStorageClientFactory($base, $request, $token); + $wrapper = $factory->createClientWrapper(new ClientOptions(branchId: '777')); + + self::assertSame('777', $wrapper->getClientOptionsReadOnly()->getBranchId()); + self::assertSame('bound-token', $wrapper->getClientOptionsReadOnly()->getToken()); + } +} diff --git a/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php b/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php new file mode 100644 index 000000000..b023e3472 --- /dev/null +++ b/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php @@ -0,0 +1,97 @@ +createArgumentMetadata($controller) as $metadata) { + if ($metadata->getName() === $arg) { + return $metadata; + } + } + + self::fail(sprintf('Controller has no argument "%s"', $arg)); + } + + public function testResolvesNothingForOtherArgumentTypes(): void + { + $controller = new class { + public function __invoke(string $foo): void + { + } + }; + + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage->expects(self::never())->method('getToken'); + + $result = $this->resolver($tokenStorage)->resolve(new Request(), $this->metadataFor($controller, 'foo')); + + self::assertSame([], [...$result]); + } + + public function testResolvesBoundFactoryBuildingClientFromSecurityToken(): void + { + $controller = new class { + public function __invoke(RequestStorageClientFactory $storage): void + { + } + }; + + $storageToken = new SecurityStorageApiToken([], 'resolved-token'); + $securityToken = $this->createMock(TokenInterface::class); + $securityToken->expects(self::once())->method('getUser')->willReturn($storageToken); + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage->expects(self::once())->method('getToken')->willReturn($securityToken); + + $result = [...$this->resolver($tokenStorage)->resolve( + new Request(), + $this->metadataFor($controller, 'storage'), + )]; + + self::assertCount(1, $result); + self::assertInstanceOf(RequestStorageClientFactory::class, $result[0]); + self::assertSame('resolved-token', $result[0]->createClientWrapper()->getClientOptionsReadOnly()->getToken()); + } + + public function testThrowsWhenNoStorageApiTokenInSecurityContext(): void + { + $controller = new class { + public function __invoke(RequestStorageClientFactory $storage): void + { + } + }; + + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage->expects(self::once())->method('getToken')->willReturn(null); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('#[StorageApiTokenAuth]'); + + [...$this->resolver($tokenStorage)->resolve(new Request(), $this->metadataFor($controller, 'storage'))]; + } +} diff --git a/libs/api-bundle/tests/config.yaml b/libs/api-bundle/tests/config.yaml index 2ad80a7be..610f3282a 100644 --- a/libs/api-bundle/tests/config.yaml +++ b/libs/api-bundle/tests/config.yaml @@ -12,3 +12,11 @@ monolog: type: stream path: php://stdout level: debug + +services: + # This minimal kernel has no SecurityBundle, so TokenStorageInterface would be missing and the + # tagged StorageApiClientResolver could not compile. Provide a plain TokenStorage (public so a + # test can seed a token). Real apps get this from SecurityBundle. + Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface: + class: Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage + public: true From d9be4fc2bee6937fdd373e2f0b502070b4ae46fe Mon Sep 17 00:00:00 2001 From: Pepa Martinec Date: Wed, 24 Jun 2026 09:51:00 +0200 Subject: [PATCH 4/9] refactor(api-bundle): merge StorageClientApiFactory into RequestStorageClientFactory --- libs/api-bundle/README.md | 42 ++---- .../KeboolaApiExtension.php | 16 +-- .../RequestStorageClientFactory.php | 33 ++++- .../StorageApiClientResolver.php | 5 +- .../StorageClientApiFactory.php | 63 --------- .../KeboolaApiExtensionTest.php | 39 ++---- .../RequestStorageClientFactoryTest.php | 73 ++++++++--- .../StorageApiClientResolverTest.php | 6 +- .../StorageClientApiFactoryTest.php | 121 ------------------ 9 files changed, 120 insertions(+), 278 deletions(-) delete mode 100644 libs/api-bundle/src/StorageApiClient/StorageClientApiFactory.php delete mode 100644 libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryTest.php diff --git a/libs/api-bundle/README.md b/libs/api-bundle/README.md index 97eea7af5..5e9ed6c4b 100644 --- a/libs/api-bundle/README.md +++ b/libs/api-bundle/README.md @@ -147,38 +147,16 @@ which the bundle requires directly, so no extra installation is needed. ## Storage API client -When `#[StorageApiTokenAuth]` is enabled, the bundle registers a -`Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory` service. Depend on it and build a -Storage `ClientWrapper` from the resolved `#[CurrentUser] StorageApiToken` — unlike the header-based -`StorageClientRequestFactory`, it uses the token already resolved by the authenticator, so it works -for programmatic (`kbc_pat_*` / `kbc_at_*`) tokens too. - -The base options are preconfigured by the bundle: the Connection (Storage API) URL is taken from the -`ServiceClient`, the logger is the shared logger, and the user agent is the configured `app_name`. -The token comes from the `StorageApiToken` you pass, the run id from the request's `X-KBC-RunId` -header, and branch / backend from an optional per-call `ClientOptions`. - -```php -public function __construct( - private readonly StorageClientApiFactory $storageClientApiFactory, -) {} - -#[StorageApiTokenAuth] -public function __invoke(#[CurrentUser] StorageApiToken $token, Request $request) -{ - $client = $this->storageClientApiFactory->createClientWrapper($request, $token)->getBasicClient(); - - // branch-aware / per-call overrides: - // $wrapper = $this->storageClientApiFactory->createClientWrapper( - // $request, $token, new ClientOptions(branchId: $branchId), - // ); -} -``` - -For controllers, you can skip the `$request`/`$token` plumbing: type-hint -`Keboola\ApiBundle\StorageApiClient\RequestStorageClientFactory` on a controller argument and the -bundle injects a factory already bound to the current request and resolved `StorageApiToken`. You -only pass optional per-call `ClientOptions`: +When `#[StorageApiTokenAuth]` is enabled, type-hint +`Keboola\ApiBundle\StorageApiClient\RequestStorageClientFactory` on a controller argument; the bundle +injects a factory already bound to the current request and the resolved `StorageApiToken`. Unlike the +header-based `StorageClientRequestFactory`, it uses the token resolved by the authenticator, so it +works for programmatic (`kbc_pat_*` / `kbc_at_*`) tokens too. + +The base options are preconfigured by the bundle: the Connection (Storage API) URL from the +`ServiceClient`, the shared logger, and the configured `app_name` as user agent. The run id comes +from the request's `X-KBC-RunId` header; branch / backend come from an optional per-call +`ClientOptions`. ```php #[StorageApiTokenAuth] diff --git a/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php b/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php index e7eb7c968..33200d101 100644 --- a/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php +++ b/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php @@ -11,7 +11,6 @@ use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenAuthenticator; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenFactory; use Keboola\ApiBundle\StorageApiClient\StorageApiClientResolver; -use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory; use Keboola\ManageApi\Client as ManageApiClient; use Keboola\ServiceClient\ServiceClient; use Keboola\ServiceClient\ServiceDnsType; @@ -98,8 +97,9 @@ private function setupStorageApiAuthenticator( ; $authenticators[StorageApiTokenAuth::class] = new Reference(StorageApiTokenAuthenticator::class); - // Storage API client factory available to controllers/services for building a ClientWrapper - // from the resolved StorageApiToken. + // Base Storage client options preconfigured by the bundle: Connection URL from the + // ServiceClient (resolved at runtime), the shared logger, and the app name as user agent. + // Token, run id, branch and backend stay per-request / per-call. $connectionUrl = (new Definition()) ->setFactory([new Reference(ServiceClient::class), 'getConnectionServiceUrl']); @@ -108,15 +108,11 @@ private function setupStorageApiAuthenticator( ->setArgument('$logger', new Reference('logger')) ->setArgument('$userAgent', $config['app_name']); - $container->register(StorageClientApiFactory::class) - ->setArgument('$clientOptions', $baseClientOptions) - ; - // Controller-argument value resolver that hands controllers a RequestStorageClientFactory - // bound to the current request and the resolved StorageApiToken. Triggers purely on the - // argument type; the token comes from security's TokenStorage (same source as #[CurrentUser]). + // bound to the current request and the resolved StorageApiToken (from security's TokenStorage, + // same source as #[CurrentUser]). Triggers purely on the argument type. $container->register(StorageApiClientResolver::class) - ->setArgument('$factory', new Reference(StorageClientApiFactory::class)) + ->setArgument('$baseClientOptions', $baseClientOptions) ->setArgument('$tokenStorage', new Reference(TokenStorageInterface::class)) ->addTag('controller.argument_value_resolver') ; diff --git a/libs/api-bundle/src/StorageApiClient/RequestStorageClientFactory.php b/libs/api-bundle/src/StorageApiClient/RequestStorageClientFactory.php index ff9ad85c6..8756b2213 100644 --- a/libs/api-bundle/src/StorageApiClient/RequestStorageClientFactory.php +++ b/libs/api-bundle/src/StorageApiClient/RequestStorageClientFactory.php @@ -5,14 +5,17 @@ namespace Keboola\ApiBundle\StorageApiClient; use Keboola\StorageApiBranch\ClientWrapper; +use Keboola\StorageApiBranch\Factory\AuthType; use Keboola\StorageApiBranch\Factory\ClientOptions; use Keboola\StorageApiBranch\StorageApiToken; use Symfony\Component\HttpFoundation\Request; class RequestStorageClientFactory { + public const RUN_ID_HEADER = 'X-KBC-RunId'; + public function __construct( - private readonly StorageClientApiFactory $factory, + private readonly ClientOptions $baseClientOptions, private readonly Request $request, private readonly StorageApiToken $token, ) { @@ -20,6 +23,32 @@ public function __construct( public function createClientWrapper(?ClientOptions $clientOptions = null): ClientWrapper { - return $this->factory->createClientWrapper($this->request, $this->token, $clientOptions); + $options = clone $this->baseClientOptions; + if ($clientOptions !== null) { + $options->addValuesFrom($clientOptions); + } + + $options->setToken($this->token->getTokenValue()); + $options->setAuthType(AuthType::STORAGE_TOKEN); + $options->setRunId($this->getRunId($options)); + + return new ClientWrapper($options); + } + + private function getRunId(ClientOptions $options): string + { + $runId = (string) $this->request->headers->get(self::RUN_ID_HEADER); + + if ($runId === '') { + $runIdGenerator = $options->getRunIdGenerator(); + if ($runIdGenerator !== null) { + $runId = $runIdGenerator($options); + assert(is_string($runId)); + } else { + $runId = uniqid('run-'); + } + } + + return $runId; } } diff --git a/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php b/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php index fd8528414..2bad69e4b 100644 --- a/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php +++ b/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php @@ -4,6 +4,7 @@ namespace Keboola\ApiBundle\StorageApiClient; +use Keboola\StorageApiBranch\Factory\ClientOptions; use Keboola\StorageApiBranch\StorageApiToken; use RuntimeException; use Symfony\Component\HttpFoundation\Request; @@ -14,7 +15,7 @@ class StorageApiClientResolver implements ValueResolverInterface { public function __construct( - private readonly StorageClientApiFactory $factory, + private readonly ClientOptions $baseClientOptions, private readonly TokenStorageInterface $tokenStorage, ) { } @@ -34,6 +35,6 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable )); } - return [new RequestStorageClientFactory($this->factory, $request, $user)]; + return [new RequestStorageClientFactory($this->baseClientOptions, $request, $user)]; } } diff --git a/libs/api-bundle/src/StorageApiClient/StorageClientApiFactory.php b/libs/api-bundle/src/StorageApiClient/StorageClientApiFactory.php deleted file mode 100644 index 8f433ef83..000000000 --- a/libs/api-bundle/src/StorageApiClient/StorageClientApiFactory.php +++ /dev/null @@ -1,63 +0,0 @@ -clientOptions = new ClientOptions(); - $this->clientOptions->addValuesFrom($clientOptions); - } - - public function getClientOptionsReadOnly(): ClientOptions - { - return clone $this->clientOptions; - } - - public function createClientWrapper( - Request $request, - StorageApiToken $storageToken, - ?ClientOptions $clientOptions = null, - ): ClientWrapper { - $options = clone $this->clientOptions; - if ($clientOptions) { - $options->addValuesFrom($clientOptions); - } - - $options->setToken($storageToken->getTokenValue()); - $options->setAuthType(AuthType::STORAGE_TOKEN); - $options->setRunId($this->getRunId($request, $options)); - - return new ClientWrapper($options); - } - - private function getRunId(Request $request, ClientOptions $options): string - { - $runId = (string) $request->headers->get(self::RUN_ID_HEADER); - - if ($runId === '') { - $runIdGenerator = $options->getRunIdGenerator(); - if ($runIdGenerator !== null) { - $runId = $runIdGenerator($options); - assert(is_string($runId)); - } else { - $runId = uniqid('run-'); - } - } - return $runId; - } -} diff --git a/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php b/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php index 0a437bd29..32e83eb34 100644 --- a/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php +++ b/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php @@ -9,7 +9,6 @@ use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenAuthenticator; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenFactory; use Keboola\ApiBundle\StorageApiClient\StorageApiClientResolver; -use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory; use Keboola\ManageApi\Client as ManageApiClient; use Keboola\ServiceClient\ServiceClient; use Keboola\StorageApiBranch\Factory\ClientOptions; @@ -58,52 +57,38 @@ public function testStorageApiServicesAreRegistered(): void ); } - public function testStorageClientApiFactoryIsRegisteredWithBaseOptions(): void + public function testStorageApiClientResolverIsRegisteredWithBaseOptionsAndTagged(): void { $container = $this->buildContainer([['app_name' => 'storage-test-app']]); self::assertTrue( - $container->hasDefinition(StorageClientApiFactory::class), - 'StorageClientApiFactory must be registered', + $container->hasDefinition(StorageApiClientResolver::class), + 'StorageApiClientResolver must be registered', ); - $clientOptions = $container->getDefinition(StorageClientApiFactory::class)->getArgument('$clientOptions'); - self::assertInstanceOf(Definition::class, $clientOptions); - self::assertSame(ClientOptions::class, $clientOptions->getClass()); + $definition = $container->getDefinition(StorageApiClientResolver::class); + self::assertArrayHasKey('controller.argument_value_resolver', $definition->getTags()); + + $baseClientOptions = $definition->getArgument('$baseClientOptions'); + self::assertInstanceOf(Definition::class, $baseClientOptions); + self::assertSame(ClientOptions::class, $baseClientOptions->getClass()); // userAgent is the configured app name - self::assertSame('storage-test-app', $clientOptions->getArgument('$userAgent')); + self::assertSame('storage-test-app', $baseClientOptions->getArgument('$userAgent')); // logger is the shared @logger service - $logger = $clientOptions->getArgument('$logger'); + $logger = $baseClientOptions->getArgument('$logger'); self::assertInstanceOf(Reference::class, $logger); self::assertSame('logger', (string) $logger); // url is resolved at runtime from ServiceClient::getConnectionServiceUrl() - $url = $clientOptions->getArgument('$url'); + $url = $baseClientOptions->getArgument('$url'); self::assertInstanceOf(Definition::class, $url); $urlFactory = $url->getFactory(); self::assertIsArray($urlFactory); self::assertInstanceOf(Reference::class, $urlFactory[0]); self::assertSame(ServiceClient::class, (string) $urlFactory[0]); self::assertSame('getConnectionServiceUrl', $urlFactory[1]); - } - - public function testStorageApiClientResolverIsRegisteredAndTagged(): void - { - $container = $this->buildContainer([[]]); - - self::assertTrue( - $container->hasDefinition(StorageApiClientResolver::class), - 'StorageApiClientResolver must be registered', - ); - - $definition = $container->getDefinition(StorageApiClientResolver::class); - self::assertArrayHasKey('controller.argument_value_resolver', $definition->getTags()); - - $factory = $definition->getArgument('$factory'); - self::assertInstanceOf(Reference::class, $factory); - self::assertSame(StorageClientApiFactory::class, (string) $factory); $tokenStorage = $definition->getArgument('$tokenStorage'); self::assertInstanceOf(Reference::class, $tokenStorage); diff --git a/libs/api-bundle/tests/StorageApiClient/RequestStorageClientFactoryTest.php b/libs/api-bundle/tests/StorageApiClient/RequestStorageClientFactoryTest.php index 33e46e221..0113e5c28 100644 --- a/libs/api-bundle/tests/StorageApiClient/RequestStorageClientFactoryTest.php +++ b/libs/api-bundle/tests/StorageApiClient/RequestStorageClientFactoryTest.php @@ -5,7 +5,7 @@ namespace Keboola\ApiBundle\Tests\StorageApiClient; use Keboola\ApiBundle\StorageApiClient\RequestStorageClientFactory; -use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory; +use Keboola\StorageApiBranch\Factory\AuthType; use Keboola\StorageApiBranch\Factory\ClientOptions; use Keboola\StorageApiBranch\StorageApiToken; use PHPUnit\Framework\TestCase; @@ -13,29 +13,70 @@ class RequestStorageClientFactoryTest extends TestCase { - public function testCreateClientWrapperUsesBoundRequestAndToken(): void + private static function factory(ClientOptions $baseClientOptions, Request $request): RequestStorageClientFactory + { + return new RequestStorageClientFactory($baseClientOptions, $request, new StorageApiToken([], 'bound-token')); + } + + public function testCreateClientWrapperUsesBoundTokenWithStorageTokenAuth(): void + { + $factory = self::factory(new ClientOptions('https://connection.test'), new Request()); + + $options = $factory->createClientWrapper()->getClientOptionsReadOnly(); + + self::assertSame('bound-token', $options->getToken()); + self::assertSame(AuthType::STORAGE_TOKEN, $options->getAuthType()); + } + + public function testRunIdTakenFromRequestHeaderWhenPresent(): void { $request = new Request([], [], [], [], [], ['HTTP_X_KBC_RUNID' => '42']); - $token = new StorageApiToken([], 'bound-token'); - $base = new StorageClientApiFactory(new ClientOptions('https://connection.test')); + $factory = self::factory(new ClientOptions('https://connection.test'), $request); + + self::assertSame('42', $factory->createClientWrapper()->getClientOptionsReadOnly()->getRunId()); + } + + public function testRunIdFallsBackToGeneratedValueWhenHeaderMissing(): void + { + $factory = self::factory(new ClientOptions('https://connection.test'), new Request()); + + self::assertStringStartsWith( + 'run-', + (string) $factory->createClientWrapper()->getClientOptionsReadOnly()->getRunId(), + ); + } + + public function testRunIdGeneratorUsedWhenHeaderMissing(): void + { + $baseOptions = new ClientOptions('https://connection.test'); + $baseOptions->setRunIdGenerator(fn (ClientOptions $o): string => 'gen-' . $o->getUrl()); + $factory = self::factory($baseOptions, new Request()); + + self::assertSame( + 'gen-https://connection.test', + $factory->createClientWrapper()->getClientOptionsReadOnly()->getRunId(), + ); + } + + public function testPerCallClientOptionsAreMergedOverBase(): void + { + $factory = self::factory(new ClientOptions('https://connection.test'), new Request()); - $factory = new RequestStorageClientFactory($base, $request, $token); - $wrapper = $factory->createClientWrapper(); + $options = $factory->createClientWrapper(new ClientOptions(branchId: '777'))->getClientOptionsReadOnly(); - self::assertSame('bound-token', $wrapper->getClientOptionsReadOnly()->getToken()); - self::assertSame('42', $wrapper->getClientOptionsReadOnly()->getRunId()); + self::assertSame('777', $options->getBranchId()); + self::assertSame('https://connection.test', $options->getUrl()); + self::assertSame('bound-token', $options->getToken()); } - public function testCreateClientWrapperMergesPerCallOptions(): void + public function testBaseOptionsAreNotMutated(): void { - $request = new Request(); - $token = new StorageApiToken([], 'bound-token'); - $base = new StorageClientApiFactory(new ClientOptions('https://connection.test')); + $baseOptions = new ClientOptions('https://connection.test'); + $factory = self::factory($baseOptions, new Request()); - $factory = new RequestStorageClientFactory($base, $request, $token); - $wrapper = $factory->createClientWrapper(new ClientOptions(branchId: '777')); + $factory->createClientWrapper(); - self::assertSame('777', $wrapper->getClientOptionsReadOnly()->getBranchId()); - self::assertSame('bound-token', $wrapper->getClientOptionsReadOnly()->getToken()); + self::assertNull($baseOptions->getToken()); + self::assertNull($baseOptions->getAuthType()); } } diff --git a/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php b/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php index b023e3472..a18a91596 100644 --- a/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php +++ b/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php @@ -7,7 +7,6 @@ use Keboola\ApiBundle\Security\StorageApiToken\StorageApiToken as SecurityStorageApiToken; use Keboola\ApiBundle\StorageApiClient\RequestStorageClientFactory; use Keboola\ApiBundle\StorageApiClient\StorageApiClientResolver; -use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory; use Keboola\StorageApiBranch\Factory\ClientOptions; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -21,10 +20,7 @@ class StorageApiClientResolverTest extends TestCase { private function resolver(TokenStorageInterface $tokenStorage): StorageApiClientResolver { - return new StorageApiClientResolver( - new StorageClientApiFactory(new ClientOptions('https://connection.test')), - $tokenStorage, - ); + return new StorageApiClientResolver(new ClientOptions('https://connection.test'), $tokenStorage); } private function metadataFor(object $controller, string $arg): ArgumentMetadata diff --git a/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryTest.php b/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryTest.php deleted file mode 100644 index 49702536f..000000000 --- a/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryTest.php +++ /dev/null @@ -1,121 +0,0 @@ - 'header-storage-token', - self::AUTHORIZATION_HEADER => 'Bearer kbc_pat_from_header', - ]); - $factory = new StorageClientApiFactory(new ClientOptions('https://connection.test')); - - $wrapper = $factory->createClientWrapper($request, self::storageApiToken('resolved-storage-token')); - - self::assertSame('resolved-storage-token', $wrapper->getClientOptionsReadOnly()->getToken()); - self::assertSame(AuthType::STORAGE_TOKEN, $wrapper->getClientOptionsReadOnly()->getAuthType()); - } - - public function testRunIdTakenFromHeaderWhenPresent(): void - { - $request = new Request([], [], [], [], [], [self::RUN_ID_HEADER => '123']); - $factory = new StorageClientApiFactory(new ClientOptions('https://connection.test')); - - $wrapper = $factory->createClientWrapper($request, self::storageApiToken('t')); - - self::assertSame('123', $wrapper->getClientOptionsReadOnly()->getRunId()); - } - - public function testRunIdFallsBackToGeneratedValueWhenHeaderMissing(): void - { - $request = new Request(); - $factory = new StorageClientApiFactory(new ClientOptions('https://connection.test')); - - $wrapper = $factory->createClientWrapper($request, self::storageApiToken('t')); - - self::assertStringStartsWith('run-', (string) $wrapper->getClientOptionsReadOnly()->getRunId()); - } - - public function testRunIdGeneratorUsedWhenHeaderMissing(): void - { - $request = new Request(); - $options = new ClientOptions('https://connection.test'); - $options->setRunIdGenerator(fn (ClientOptions $o): string => 'gen-' . $o->getUrl()); - $factory = new StorageClientApiFactory($options); - - $wrapper = $factory->createClientWrapper($request, self::storageApiToken('t')); - - self::assertSame('gen-https://connection.test', $wrapper->getClientOptionsReadOnly()->getRunId()); - } - - public function testPerCallClientOptionsAreMergedOverBase(): void - { - $request = new Request(); - $factory = new StorageClientApiFactory(new ClientOptions('https://connection.test')); - - $wrapper = $factory->createClientWrapper( - $request, - self::storageApiToken('t'), - new ClientOptions(branchId: '1234'), - ); - - self::assertSame('1234', $wrapper->getClientOptionsReadOnly()->getBranchId()); - self::assertSame('https://connection.test', $wrapper->getClientOptionsReadOnly()->getUrl()); - } - - public function testBaseOptionsAreNotMutated(): void - { - $request = new Request(); - $base = new ClientOptions('https://connection.test'); - $factory = new StorageClientApiFactory($base); - - $factory->createClientWrapper($request, self::storageApiToken('t')); - - // The token must not leak back into the caller's base options nor the factory's own copy. - self::assertNull($base->getToken()); - self::assertNull($factory->getClientOptionsReadOnly()->getToken()); - } - - public function testGetClientOptionsReadOnlyReturnsIsolatedClone(): void - { - $factory = new StorageClientApiFactory(new ClientOptions('https://foo')); - - self::assertSame('https://foo', $factory->getClientOptionsReadOnly()->getUrl()); - $factory->getClientOptionsReadOnly()->setUrl('https://bar'); - self::assertSame('https://foo', $factory->getClientOptionsReadOnly()->getUrl()); - } - - public function testAcceptsSecurityStorageApiTokenSubclass(): void - { - $request = new Request(); - $factory = new StorageClientApiFactory(new ClientOptions('https://connection.test')); - $token = new SecurityStorageApiToken([], 'subclass-token'); - - $wrapper = $factory->createClientWrapper($request, $token); - - self::assertSame('subclass-token', $wrapper->getClientOptionsReadOnly()->getToken()); - } -} From c3aa2468e1cb17bd6c72dce2dda2d74843e5571e Mon Sep 17 00:00:00 2001 From: Pepa Martinec Date: Wed, 24 Jun 2026 09:53:53 +0200 Subject: [PATCH 5/9] refactor(api-bundle): rename RequestStorageClientFactory to StorageClientApiFactory --- libs/api-bundle/README.md | 4 ++-- .../src/StorageApiClient/StorageApiClientResolver.php | 4 ++-- ...orageClientFactory.php => StorageClientApiFactory.php} | 2 +- .../StorageApiClient/StorageApiClientResolverTest.php | 8 ++++---- ...entFactoryTest.php => StorageClientApiFactoryTest.php} | 8 ++++---- 5 files changed, 13 insertions(+), 13 deletions(-) rename libs/api-bundle/src/StorageApiClient/{RequestStorageClientFactory.php => StorageClientApiFactory.php} (97%) rename libs/api-bundle/tests/StorageApiClient/{RequestStorageClientFactoryTest.php => StorageClientApiFactoryTest.php} (90%) diff --git a/libs/api-bundle/README.md b/libs/api-bundle/README.md index 5e9ed6c4b..b42b19ad2 100644 --- a/libs/api-bundle/README.md +++ b/libs/api-bundle/README.md @@ -148,7 +148,7 @@ which the bundle requires directly, so no extra installation is needed. ## Storage API client When `#[StorageApiTokenAuth]` is enabled, type-hint -`Keboola\ApiBundle\StorageApiClient\RequestStorageClientFactory` on a controller argument; the bundle +`Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory` on a controller argument; the bundle injects a factory already bound to the current request and the resolved `StorageApiToken`. Unlike the header-based `StorageClientRequestFactory`, it uses the token resolved by the authenticator, so it works for programmatic (`kbc_pat_*` / `kbc_at_*`) tokens too. @@ -160,7 +160,7 @@ from the request's `X-KBC-RunId` header; branch / backend come from an optional ```php #[StorageApiTokenAuth] -public function __invoke(RequestStorageClientFactory $storage) +public function __invoke(StorageClientApiFactory $storage) { $client = $storage->createClientWrapper()->getBasicClient(); diff --git a/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php b/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php index 2bad69e4b..7b554af20 100644 --- a/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php +++ b/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php @@ -22,7 +22,7 @@ public function __construct( public function resolve(Request $request, ArgumentMetadata $argument): iterable { - if ($argument->getType() !== RequestStorageClientFactory::class) { + if ($argument->getType() !== StorageClientApiFactory::class) { return []; } @@ -35,6 +35,6 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable )); } - return [new RequestStorageClientFactory($this->baseClientOptions, $request, $user)]; + return [new StorageClientApiFactory($this->baseClientOptions, $request, $user)]; } } diff --git a/libs/api-bundle/src/StorageApiClient/RequestStorageClientFactory.php b/libs/api-bundle/src/StorageApiClient/StorageClientApiFactory.php similarity index 97% rename from libs/api-bundle/src/StorageApiClient/RequestStorageClientFactory.php rename to libs/api-bundle/src/StorageApiClient/StorageClientApiFactory.php index 8756b2213..51d41d868 100644 --- a/libs/api-bundle/src/StorageApiClient/RequestStorageClientFactory.php +++ b/libs/api-bundle/src/StorageApiClient/StorageClientApiFactory.php @@ -10,7 +10,7 @@ use Keboola\StorageApiBranch\StorageApiToken; use Symfony\Component\HttpFoundation\Request; -class RequestStorageClientFactory +class StorageClientApiFactory { public const RUN_ID_HEADER = 'X-KBC-RunId'; diff --git a/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php b/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php index a18a91596..a7a3afd3f 100644 --- a/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php +++ b/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php @@ -5,8 +5,8 @@ namespace Keboola\ApiBundle\Tests\StorageApiClient; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiToken as SecurityStorageApiToken; -use Keboola\ApiBundle\StorageApiClient\RequestStorageClientFactory; use Keboola\ApiBundle\StorageApiClient\StorageApiClientResolver; +use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory; use Keboola\StorageApiBranch\Factory\ClientOptions; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -53,7 +53,7 @@ public function __invoke(string $foo): void public function testResolvesBoundFactoryBuildingClientFromSecurityToken(): void { $controller = new class { - public function __invoke(RequestStorageClientFactory $storage): void + public function __invoke(StorageClientApiFactory $storage): void { } }; @@ -70,14 +70,14 @@ public function __invoke(RequestStorageClientFactory $storage): void )]; self::assertCount(1, $result); - self::assertInstanceOf(RequestStorageClientFactory::class, $result[0]); + self::assertInstanceOf(StorageClientApiFactory::class, $result[0]); self::assertSame('resolved-token', $result[0]->createClientWrapper()->getClientOptionsReadOnly()->getToken()); } public function testThrowsWhenNoStorageApiTokenInSecurityContext(): void { $controller = new class { - public function __invoke(RequestStorageClientFactory $storage): void + public function __invoke(StorageClientApiFactory $storage): void { } }; diff --git a/libs/api-bundle/tests/StorageApiClient/RequestStorageClientFactoryTest.php b/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryTest.php similarity index 90% rename from libs/api-bundle/tests/StorageApiClient/RequestStorageClientFactoryTest.php rename to libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryTest.php index 0113e5c28..38a995df9 100644 --- a/libs/api-bundle/tests/StorageApiClient/RequestStorageClientFactoryTest.php +++ b/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryTest.php @@ -4,18 +4,18 @@ namespace Keboola\ApiBundle\Tests\StorageApiClient; -use Keboola\ApiBundle\StorageApiClient\RequestStorageClientFactory; +use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory; use Keboola\StorageApiBranch\Factory\AuthType; use Keboola\StorageApiBranch\Factory\ClientOptions; use Keboola\StorageApiBranch\StorageApiToken; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; -class RequestStorageClientFactoryTest extends TestCase +class StorageClientApiFactoryTest extends TestCase { - private static function factory(ClientOptions $baseClientOptions, Request $request): RequestStorageClientFactory + private static function factory(ClientOptions $baseClientOptions, Request $request): StorageClientApiFactory { - return new RequestStorageClientFactory($baseClientOptions, $request, new StorageApiToken([], 'bound-token')); + return new StorageClientApiFactory($baseClientOptions, $request, new StorageApiToken([], 'bound-token')); } public function testCreateClientWrapperUsesBoundTokenWithStorageTokenAuth(): void From df6d1e7e56ae26833f246fa6d2af2ce2e23181ad Mon Sep 17 00:00:00 2001 From: Pepa Martinec Date: Wed, 24 Jun 2026 09:57:55 +0200 Subject: [PATCH 6/9] refactor(api-bundle): rename resolver to StorageClientApiFactoryResolver --- .../src/DependencyInjection/KeboolaApiExtension.php | 4 ++-- ...esolver.php => StorageClientApiFactoryResolver.php} | 2 +- .../DependencyInjection/KeboolaApiExtensionTest.php | 10 +++++----- ...est.php => StorageClientApiFactoryResolverTest.php} | 8 ++++---- libs/api-bundle/tests/config.yaml | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) rename libs/api-bundle/src/StorageApiClient/{StorageApiClientResolver.php => StorageClientApiFactoryResolver.php} (94%) rename libs/api-bundle/tests/StorageApiClient/{StorageApiClientResolverTest.php => StorageClientApiFactoryResolverTest.php} (92%) diff --git a/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php b/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php index 33200d101..062e66f97 100644 --- a/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php +++ b/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php @@ -10,7 +10,7 @@ use Keboola\ApiBundle\Security\ApplicationToken\ManageApiClientFactory; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenAuthenticator; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenFactory; -use Keboola\ApiBundle\StorageApiClient\StorageApiClientResolver; +use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactoryResolver; use Keboola\ManageApi\Client as ManageApiClient; use Keboola\ServiceClient\ServiceClient; use Keboola\ServiceClient\ServiceDnsType; @@ -111,7 +111,7 @@ private function setupStorageApiAuthenticator( // Controller-argument value resolver that hands controllers a RequestStorageClientFactory // bound to the current request and the resolved StorageApiToken (from security's TokenStorage, // same source as #[CurrentUser]). Triggers purely on the argument type. - $container->register(StorageApiClientResolver::class) + $container->register(StorageClientApiFactoryResolver::class) ->setArgument('$baseClientOptions', $baseClientOptions) ->setArgument('$tokenStorage', new Reference(TokenStorageInterface::class)) ->addTag('controller.argument_value_resolver') diff --git a/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php b/libs/api-bundle/src/StorageApiClient/StorageClientApiFactoryResolver.php similarity index 94% rename from libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php rename to libs/api-bundle/src/StorageApiClient/StorageClientApiFactoryResolver.php index 7b554af20..87d8a70f2 100644 --- a/libs/api-bundle/src/StorageApiClient/StorageApiClientResolver.php +++ b/libs/api-bundle/src/StorageApiClient/StorageClientApiFactoryResolver.php @@ -12,7 +12,7 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -class StorageApiClientResolver implements ValueResolverInterface +class StorageClientApiFactoryResolver implements ValueResolverInterface { public function __construct( private readonly ClientOptions $baseClientOptions, diff --git a/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php b/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php index 32e83eb34..367271db9 100644 --- a/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php +++ b/libs/api-bundle/tests/DependencyInjection/KeboolaApiExtensionTest.php @@ -8,7 +8,7 @@ use Keboola\ApiBundle\Security\ApplicationToken\ManageApiClientFactory; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenAuthenticator; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenFactory; -use Keboola\ApiBundle\StorageApiClient\StorageApiClientResolver; +use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactoryResolver; use Keboola\ManageApi\Client as ManageApiClient; use Keboola\ServiceClient\ServiceClient; use Keboola\StorageApiBranch\Factory\ClientOptions; @@ -57,16 +57,16 @@ public function testStorageApiServicesAreRegistered(): void ); } - public function testStorageApiClientResolverIsRegisteredWithBaseOptionsAndTagged(): void + public function testStorageClientApiFactoryResolverIsRegisteredWithBaseOptionsAndTagged(): void { $container = $this->buildContainer([['app_name' => 'storage-test-app']]); self::assertTrue( - $container->hasDefinition(StorageApiClientResolver::class), - 'StorageApiClientResolver must be registered', + $container->hasDefinition(StorageClientApiFactoryResolver::class), + 'StorageClientApiFactoryResolver must be registered', ); - $definition = $container->getDefinition(StorageApiClientResolver::class); + $definition = $container->getDefinition(StorageClientApiFactoryResolver::class); self::assertArrayHasKey('controller.argument_value_resolver', $definition->getTags()); $baseClientOptions = $definition->getArgument('$baseClientOptions'); diff --git a/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php b/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryResolverTest.php similarity index 92% rename from libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php rename to libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryResolverTest.php index a7a3afd3f..8d5c139d4 100644 --- a/libs/api-bundle/tests/StorageApiClient/StorageApiClientResolverTest.php +++ b/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryResolverTest.php @@ -5,8 +5,8 @@ namespace Keboola\ApiBundle\Tests\StorageApiClient; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiToken as SecurityStorageApiToken; -use Keboola\ApiBundle\StorageApiClient\StorageApiClientResolver; use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory; +use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactoryResolver; use Keboola\StorageApiBranch\Factory\ClientOptions; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -16,11 +16,11 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -class StorageApiClientResolverTest extends TestCase +class StorageClientApiFactoryResolverTest extends TestCase { - private function resolver(TokenStorageInterface $tokenStorage): StorageApiClientResolver + private function resolver(TokenStorageInterface $tokenStorage): StorageClientApiFactoryResolver { - return new StorageApiClientResolver(new ClientOptions('https://connection.test'), $tokenStorage); + return new StorageClientApiFactoryResolver(new ClientOptions('https://connection.test'), $tokenStorage); } private function metadataFor(object $controller, string $arg): ArgumentMetadata diff --git a/libs/api-bundle/tests/config.yaml b/libs/api-bundle/tests/config.yaml index 610f3282a..a521137b0 100644 --- a/libs/api-bundle/tests/config.yaml +++ b/libs/api-bundle/tests/config.yaml @@ -15,7 +15,7 @@ monolog: services: # This minimal kernel has no SecurityBundle, so TokenStorageInterface would be missing and the - # tagged StorageApiClientResolver could not compile. Provide a plain TokenStorage (public so a + # tagged StorageClientApiFactoryResolver could not compile. Provide a plain TokenStorage (public so a # test can seed a token). Real apps get this from SecurityBundle. Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface: class: Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage From 394d4e0032e332d880089da083585093a3e2f0eb Mon Sep 17 00:00:00 2001 From: Pepa Martinec Date: Wed, 24 Jun 2026 12:58:27 +0200 Subject: [PATCH 7/9] feat(api-bundle): resolve nullable StorageClientApiFactory argument to null when no Storage token --- libs/api-bundle/README.md | 14 ++++++ .../StorageClientApiFactoryResolver.php | 11 ++++- .../StorageClientApiFactoryResolverTest.php | 46 ++++++++++++++++++- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/libs/api-bundle/README.md b/libs/api-bundle/README.md index b42b19ad2..b438d34dd 100644 --- a/libs/api-bundle/README.md +++ b/libs/api-bundle/README.md @@ -169,6 +169,20 @@ public function __invoke(StorageClientApiFactory $storage) } ``` +On a controller that may authenticate through another attribute too (e.g. +`#[ApplicationTokenAuth]` alongside `#[StorageApiTokenAuth]` — they are OR'd), the request can be +authenticated without a Storage token. Make the argument nullable to receive `null` in that case; +a required (non-nullable) argument throws when no Storage token is present: + +```php +#[ApplicationTokenAuth(scopes: ['something:manage'])] +#[StorageApiTokenAuth] +public function __invoke(?StorageClientApiFactory $storage) +{ + $client = $storage?->createClientWrapper()->getBasicClient(); // null when authenticated via Manage token +} +``` + ## Testing controllers `Keboola\ApiBundle\Test\AuthenticatorTestTrait` stubs the authenticators in functional diff --git a/libs/api-bundle/src/StorageApiClient/StorageClientApiFactoryResolver.php b/libs/api-bundle/src/StorageApiClient/StorageClientApiFactoryResolver.php index 87d8a70f2..7d98a5b3c 100644 --- a/libs/api-bundle/src/StorageApiClient/StorageClientApiFactoryResolver.php +++ b/libs/api-bundle/src/StorageApiClient/StorageClientApiFactoryResolver.php @@ -28,9 +28,16 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable $user = $this->tokenStorage->getToken()?->getUser(); if (!$user instanceof StorageApiToken) { + // No Storage token in the security context - e.g. the request authenticated through a + // different attribute on the same controller (#[ApplicationTokenAuth]). A nullable + // argument opts into receiving null; a required one is treated as a misconfiguration. + if ($argument->isNullable()) { + return [null]; + } + throw new RuntimeException(sprintf( - 'Cannot resolve argument "$%s": no authenticated Storage API token. ' - . 'The controller must be guarded by #[StorageApiTokenAuth].', + 'Cannot resolve argument "$%s": no authenticated Storage API token. Guard the ' + . 'controller with #[StorageApiTokenAuth], or make the argument nullable to allow null.', $argument->getName(), )); } diff --git a/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryResolverTest.php b/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryResolverTest.php index 8d5c139d4..288b4dc16 100644 --- a/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryResolverTest.php +++ b/libs/api-bundle/tests/StorageApiClient/StorageClientApiFactoryResolverTest.php @@ -74,7 +74,7 @@ public function __invoke(StorageClientApiFactory $storage): void self::assertSame('resolved-token', $result[0]->createClientWrapper()->getClientOptionsReadOnly()->getToken()); } - public function testThrowsWhenNoStorageApiTokenInSecurityContext(): void + public function testThrowsForRequiredArgumentWhenNoStorageApiToken(): void { $controller = new class { public function __invoke(StorageClientApiFactory $storage): void @@ -90,4 +90,48 @@ public function __invoke(StorageClientApiFactory $storage): void [...$this->resolver($tokenStorage)->resolve(new Request(), $this->metadataFor($controller, 'storage'))]; } + + public function testResolvesNullForNullableArgumentWhenNoStorageApiToken(): void + { + // Dual-guarded controller (e.g. also #[ApplicationTokenAuth]) authenticated through the + // other path: the security user is not a StorageApiToken, so a nullable argument gets null. + $controller = new class { + public function __invoke(?StorageClientApiFactory $storage): void + { + } + }; + + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage->expects(self::once())->method('getToken')->willReturn(null); + + $result = [...$this->resolver($tokenStorage)->resolve( + new Request(), + $this->metadataFor($controller, 'storage'), + )]; + + self::assertSame([null], $result); + } + + public function testResolvesFactoryForNullableArgumentWhenStorageApiTokenPresent(): void + { + $controller = new class { + public function __invoke(?StorageClientApiFactory $storage): void + { + } + }; + + $storageToken = new SecurityStorageApiToken([], 'resolved-token'); + $securityToken = $this->createMock(TokenInterface::class); + $securityToken->expects(self::once())->method('getUser')->willReturn($storageToken); + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $tokenStorage->expects(self::once())->method('getToken')->willReturn($securityToken); + + $result = [...$this->resolver($tokenStorage)->resolve( + new Request(), + $this->metadataFor($controller, 'storage'), + )]; + + self::assertCount(1, $result); + self::assertInstanceOf(StorageClientApiFactory::class, $result[0]); + } } From 7c5f9a04a1f6c36358df6f5d6f2974dd3d44c9bd Mon Sep 17 00:00:00 2001 From: Pepa Martinec Date: Wed, 24 Jun 2026 13:13:34 +0200 Subject: [PATCH 8/9] refactor(api-bundle): simplify StorageClientApiFactoryResolver control flow --- .../StorageClientApiFactoryResolver.php | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/libs/api-bundle/src/StorageApiClient/StorageClientApiFactoryResolver.php b/libs/api-bundle/src/StorageApiClient/StorageClientApiFactoryResolver.php index 7d98a5b3c..e25f783ed 100644 --- a/libs/api-bundle/src/StorageApiClient/StorageClientApiFactoryResolver.php +++ b/libs/api-bundle/src/StorageApiClient/StorageClientApiFactoryResolver.php @@ -27,21 +27,18 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable } $user = $this->tokenStorage->getToken()?->getUser(); - if (!$user instanceof StorageApiToken) { - // No Storage token in the security context - e.g. the request authenticated through a - // different attribute on the same controller (#[ApplicationTokenAuth]). A nullable - // argument opts into receiving null; a required one is treated as a misconfiguration. - if ($argument->isNullable()) { - return [null]; - } - - throw new RuntimeException(sprintf( - 'Cannot resolve argument "$%s": no authenticated Storage API token. Guard the ' - . 'controller with #[StorageApiTokenAuth], or make the argument nullable to allow null.', - $argument->getName(), - )); + if ($user instanceof StorageApiToken) { + return [new StorageClientApiFactory($this->baseClientOptions, $request, $user)]; } - return [new StorageClientApiFactory($this->baseClientOptions, $request, $user)]; + if ($argument->isNullable()) { + return [null]; + } + + throw new RuntimeException(sprintf( + 'Cannot resolve argument "$%s": no authenticated Storage API token. Guard the ' + . 'controller with #[StorageApiTokenAuth], or make the argument nullable to allow null.', + $argument->getName(), + )); } } From ea8b61c1436dbbd70fbd37d50affe65f1060f488 Mon Sep 17 00:00:00 2001 From: Pepa Martinec Date: Wed, 24 Jun 2026 13:14:23 +0200 Subject: [PATCH 9/9] chore(api-bundle): Simplify comments --- .../src/DependencyInjection/KeboolaApiExtension.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php b/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php index 062e66f97..159331654 100644 --- a/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php +++ b/libs/api-bundle/src/DependencyInjection/KeboolaApiExtension.php @@ -10,6 +10,7 @@ use Keboola\ApiBundle\Security\ApplicationToken\ManageApiClientFactory; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenAuthenticator; use Keboola\ApiBundle\Security\StorageApiToken\StorageApiTokenFactory; +use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactory; use Keboola\ApiBundle\StorageApiClient\StorageClientApiFactoryResolver; use Keboola\ManageApi\Client as ManageApiClient; use Keboola\ServiceClient\ServiceClient; @@ -97,9 +98,7 @@ private function setupStorageApiAuthenticator( ; $authenticators[StorageApiTokenAuth::class] = new Reference(StorageApiTokenAuthenticator::class); - // Base Storage client options preconfigured by the bundle: Connection URL from the - // ServiceClient (resolved at runtime), the shared logger, and the app name as user agent. - // Token, run id, branch and backend stay per-request / per-call. + // StorageClientApiFactory controller-argument value resolver $connectionUrl = (new Definition()) ->setFactory([new Reference(ServiceClient::class), 'getConnectionServiceUrl']); @@ -108,9 +107,6 @@ private function setupStorageApiAuthenticator( ->setArgument('$logger', new Reference('logger')) ->setArgument('$userAgent', $config['app_name']); - // Controller-argument value resolver that hands controllers a RequestStorageClientFactory - // bound to the current request and the resolved StorageApiToken (from security's TokenStorage, - // same source as #[CurrentUser]). Triggers purely on the argument type. $container->register(StorageClientApiFactoryResolver::class) ->setArgument('$baseClientOptions', $baseClientOptions) ->setArgument('$tokenStorage', new Reference(TokenStorageInterface::class))