keboola/api-bundle 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

keboola/api-bundle

Composer 安装命令:

composer require keboola/api-bundle

包简介

Keboola API Bundle

README 文档

README

Symfony bundle providing common functionality for Keboola API applications.

Installation

Install the package with Composer:

composer require keboola/api-bundle

Configuration

The bundle expects having %app_name% parameter defined in your Symfony configuration.

Default configuration

keboola_api:
  app_name: '%app_name%'           # application name to use in user agent
  default_service_dns_type: public # default service DNS type to use in ServiceClient, can be 'public' or 'private'

Features

Preconfigured ServiceClient

The bundle provides a preconfigured ServiceClient that can be used to resolve Keboola API URLs. By default, it is configured to use public hostnames, but it can be reconfigured to use internal ones.

keboola_api:
  default_service_dns_type: internal

Using ENV variables

If you need to use ENV variable to configure the default_service_dns_type, make sure you provide some default value, otherwise the validation will fail with error The value "" is not allowed for path "keboola_api.default_service_dns_type".

parameters:
  env(API_DNS_TYPE): internal

keboola_api:
  default_service_dns_type: '%env(API_DNS_TYPE)%'

Controller authentication using attributes

To use authentication using attributes, configure firewall to use the keboola.api_bundle.security.attribute_authenticator:

security:
  firewalls:
    attribute:
        lazy: true
        stateless: true
        custom_authenticators:
          - keboola.api_bundle.security.attribute_authenticator

Then add any combination of authentication attributes to your controller:

use Keboola\ApiBundle\Attribute\StorageApiTokenAuth;
use Keboola\ApiBundle\Security\StorageApiToken\SecurityApiToken;
use Symfony\Component\Security\Http\Attribute\CurrentUser;

#[StorageApiTokenAuth]
class Controller {
  public function __invoke(#[CurrentUser] StorageApiToken $token) 
  {
    // only requests with valid X-StorageApi-Token will be allowed
  }
}

#[StorageApiTokenAuth(features: ['feat-a', 'feat-b'])]
class Controller {
  public function __invoke(#[CurrentUser] StorageApiToken $token) 
  {
    // only requests with valid X-StorageApi-Token and project features 'feat-a' AND 'feat-b' is allowed
  }
}

#[StorageApiTokenAuth(features: ['feat-a'])]
#[StorageApiTokenAuth(features: ['feat-b'])]
class Controller {
  public function __invoke(#[CurrentUser] StorageApiToken $token) 
  {
    // only requests with valid X-StorageApi-Token and any of project features 'feat-a' OR 'feat-b' ise allowed
  }
}

#[ApplicationTokenAuth(scopes: ['something:manage'])]
#[StorageApiTokenAuth]
class Controller {
  public function __invoke(
    string $entityId,
    #[CurrentUser] TokenInterface $token,
  ) {
    // allows request with a valid Manage API token (`X-KBC-ManageApiToken`) or a Kubernetes
    // ServiceAccount JWT (`X-Kubernetes-Authorization: Bearer <jwt>`) with the 'something:manage'
    // scope, OR any valid X-StorageApi-Token — but with additional checks in controller
    $entity = $this->fetchEntity($entityId);
    if ($token instanceof StorageApiToken && $token->getProjectId() !== $entity->getProjectId()) {
      throw new AccessDeniedHttpException('...');
    }
  }
}

ApplicationTokenAuth accepts both the Manage API token header (X-KBC-ManageApiToken) and the Kubernetes ServiceAccount JWT header (X-Kubernetes-Authorization); Connection resolves both to a Manage token, so scopes/isSuperAdmin checks are identical regardless of which header the request carries.

Connection programmatic tokens (Storage token exchange)

#[StorageApiTokenAuth] transparently accepts the new Connection programmatic bearer tokens (kbc_at_* access tokens and kbc_pat_* personal access tokens) in addition to the legacy X-StorageApi-Token. Programmatic tokens are exchanged for a legacy Storage token via the Manage API client's Client::resolveStorageToken() (which calls Connection's internal resolver endpoint), authenticating with the service's own projected Kubernetes ServiceAccount JWT. The resolver returns the legacy token together with its full token detail (the same payload as Storage's tokens/verify), so the exchange is a single HTTP call — no follow-up Storage verification. The result is a normal StorageApiToken, so controllers and #[CurrentUser] StorageApiToken keep working unchanged — no controller change and no configuration switch.

Callers send Authorization: Bearer kbc_at_…/kbc_pat_… together with an X-KBC-ProjectId header (the new tokens are not project-scoped on their own).

#[StorageApiTokenAuth]
class MyController {
  public function __invoke(#[CurrentUser] StorageApiToken $token) {
    // accepts X-StorageApi-Token (legacy) OR Authorization: Bearer kbc_at_/kbc_pat_ (+ X-KBC-ProjectId)
  }
}

This requires the service's ServiceAccount to be mapped to the internal:auth-bridge:resolve-storage-token scope in Connection's Kubernetes-auth config. See docs/storage-token-exchange.md for the full design, the resolver contract, error mapping, and infrastructure prerequisites.

To use individual authentication attributes, you need to install appropriate client package:

  • to use StorageApiTokenAuth, install keboola/storage-api-client

ApplicationTokenAuth and the Storage token exchange rely on keboola/kbc-manage-api-php-client, which the bundle requires directly, so no extra installation is needed.

Note

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, type-hint 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.

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.

The client uses the auth type the request was authenticated with: a legacy or exchanged Storage token is sent as X-StorageApi-Token, while an OAuth token (Authorization: Bearer …) is sent with the bearer scheme. StorageApiToken::getTokenType() exposes it (AuthType::STORAGE_TOKEN / AuthType::BEARER) so a token value carried as a bearer token is not mistaken for a Storage token if you build a client yourself.

#[StorageApiTokenAuth]
public function __invoke(StorageClientApiFactory $storage)
{
    $client = $storage->createClientWrapper()->getBasicClient();

    // branch-aware / per-call overrides:
    // $wrapper = $storage->createClientWrapper(new ClientOptions(branchId: $branchId));
}

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:

#[ApplicationTokenAuth(scopes: ['something:manage'])]
#[StorageApiTokenAuth]
public function __invoke(?StorageClientApiFactory $storage)
{
    $client = $storage?->createClientWrapper()->getBasicClient(); // null when authenticated via Manage token
}

Configuring the base client options

The base ClientOptions (Connection URL, @logger, app_name user agent) can be tuned with the optional storage_client_options node. Values are merged onto the bundle-built base — you never have to reproduce the Connection URL resolution.

Set individual options (only YAML-expressible options are supported here):

keboola_api:
  storage_client_options:
    backoff_max_tries: 10
    aws_retries: 5
    aws_debug: false
    retry_on_maintenance: true
    use_branch_storage: false
    user_agent: my-service/1.0
    logger: monolog.logger.storage_api   # a service id for a PSR LoggerInterface

Or reference a fully-custom ClientOptions service — the only way to set options that cannot be expressed in YAML (jobPollRetryDelay / runIdGenerator closures, BackendConfiguration):

keboola_api:
  storage_client_options: app.storage_client_options   # service id
// services.php
$services->set('app.storage_client_options', ClientOptions::class)
    ->args([
        '$backoffMaxTries' => 10,
        '$jobPollRetryDelay' => /* Closure */,
    ]);

The service's non-null values are merged over the bundle base via ClientOptions::addValuesFrom(). The service (string) form and the individual options are mutually exclusive.

Testing controllers

Keboola\ApiBundle\Test\AuthenticatorTestTrait stubs the authenticators in functional (WebTestCase) tests so guarded controllers can be exercised without reaching real Storage/Manage APIs. It provides five helpers:

Helper Stubs auth for Returns
setupFakeStorageApiToken(tokenString?, projectId, features, adminId?) #[StorageApiTokenAuth] via legacy X-StorageApi-Token (resolves to an AuthType::STORAGE_TOKEN token) StorageApiToken
setupFakeOAuthToken(tokenString?, projectId, features, adminId?) #[StorageApiTokenAuth] via an OAuth Authorization: Bearer token (resolves to an AuthType::BEARER token) StorageApiToken
setupFakeConnectionToken(projectId, features, tokenString?, adminId?) #[StorageApiTokenAuth] via a kbc_at_/kbc_pat_ programmatic token (stubs the exchange resolver client) StorageApiToken
setupFakeManageApiToken(tokenString, scopes, features) #[ApplicationTokenAuth] (both the X-KBC-ManageApiToken header and the Kubernetes ServiceAccount JWT) void
bootCleanClient() — boots a fresh, reboot-disabled KernelBrowser on a clean container KernelBrowser

Why bootCleanClient()

The setupFake*Token() helpers replace services in the test container via getContainer()->set(...). That only works while those services are not yet initialized: a #[StorageApiTokenAuth] request initializes ManageApiClientFactory (it backs the programmatic-token exchange resolver), and an initialized service can no longer be replaced. So whenever a request has already run in the test — or you simply want a guaranteed-clean container — call self::bootCleanClient() first. It boots a fresh kernel, disables client reboot, and returns the KernelBrowser to use for the request.

Important

bootCleanClient() reboots the kernel, discarding the previous container. Call it before every getContainer()->set(...) the test relies on — including your own app-specific service mocks, not just the setupFake*Token() helpers. Mocks registered before bootCleanClient() are thrown away by the reboot and will not be seen by the request.

Recommended order: seed the database → bootCleanClient() → register service mocks → setupFake*Token() → request.

Requirements

  • The test case must extend Symfony's WebTestCase (bootCleanClient() uses bootKernel() / getClient() / the test.client service).
  • symfony/browser-kit must be installed (dev dependency) for the KernelBrowser client.

Example

use Keboola\ApiBundle\Test\AuthenticatorTestTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class MyActionTest extends WebTestCase
{
    use AuthenticatorTestTrait;

    public function testUnauthorized(): void
    {
        // A request that doesn't fake a token can use the standard client.
        $client = static::createClient();
        $client->request('GET', '/my-endpoint');

        self::assertResponseStatusCodeSame(401);
    }

    public function testAuthorized(): void
    {
        self::setupDatabase([$entity]);          // 1) seed state the controller reads

        $client = self::bootCleanClient();       // 2) clean container BEFORE any ->set()

        $myService = $this->createMock(MyService::class);
        self::getContainer()->set(MyService::class, $myService);   // 3) app service mocks

        $token = $this->setupFakeStorageApiToken( // 4) stub the authenticator
            projectId: '123',
            features: ['my-feature'],
        );

        $client->request('GET', '/my-endpoint', server: [   // 5) authenticated request
            'HTTP_X_STORAGEAPI_TOKEN' => $token->getTokenValue(),
        ]);

        self::assertResponseIsSuccessful();
    }
}

Swap setupFakeStorageApiToken() for setupFakeConnectionToken() (programmatic token) or setupFakeManageApiToken() (#[ApplicationTokenAuth]) depending on the attribute under test; the clean-client ordering is the same for all three.

License

MIT licensed, see LICENSE file.

keboola/api-bundle 适用场景与选型建议

keboola/api-bundle 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 13.81k 次下载、GitHub Stars 达 0, 最近一次更新时间为 2023 年 04 月 26 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 keboola/api-bundle 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 keboola/api-bundle 我们能提供哪些服务?
定制开发 / 二次开发

基于 keboola/api-bundle 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 13.81k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 14
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 13
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2023-04-26