dmstr / api-configuration-bundle
Configurable external API connections: ApiConfiguration entity with type+credentials, health-check + OAuth-authorize flow, ApiExtensionRegistry adapter pattern.
Package info
github.com/dmstr/api-configuration-bundle
Type:symfony-bundle
pkg:composer/dmstr/api-configuration-bundle
Requires
- php: >=8.4
- api-platform/symfony: ^4.0
- dmstr/api-platform-utils-bundle: *
- dmstr/openapi-json-schema-bundle: *
- doctrine/doctrine-bundle: ^2.8
- doctrine/orm: ^3.0
- opis/json-schema: ^2.3
- symfony/framework-bundle: ^7.0
- symfony/validator: ^7.0|^8.0
Requires (Dev)
- ext-pdo_sqlite: *
- doctrine/dbal: ^3.7
- doctrine/migrations: ^3.9
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-10 10:41:56 UTC
README
dmstr/api-configuration-bundle
Manage external API connections as Doctrine entities.
Features
ApiConfigurationentity (name,type,endpointType,configJson,active) under/api/admin/api_configurations,configJsonvalidated against the JSON schema of its type- Secrets in
configJsonencrypted at rest and masked on read, see Security - Sub-resources
GET /api/admin/api_configurations/{id}/healthandGET /api/admin/api_configurations/{id}/authorize, OAuth callback at/api/oauth/callback ApiExtensionRegistry: extensions implementApiExtensionInterface(andAuthorizableExtensionInterfacefor OAuth) and are registered by tag- Console commands
app:api:create,app:api:test-connection,app:api:validate-file,app:api-configuration:healthandapp:api-configuration:encrypt-secrets
Changes per version are listed in CHANGELOG.md.
API clients
An extension implements ApiExtensionInterface (tagged automatically) and builds a client implementing ApiClientInterface, which holds only what every source supports. Optional capabilities are separate interfaces; implement those the source really supports, callers check instanceof:
| Interface | Methods |
|---|---|
CustomerAwareApiClientInterface |
getCustomers() |
TodoListAwareApiClientInterface |
getTodoLists() |
ChangeProbeInterface |
hasChanges() — cheap, fail open; without it a source counts as always changed |
UserAwareApiClientInterface |
getUsers() |
authenticate() returns nothing and throws AuthenticationFailedException with the original exception as previous.
ApiClientFactory::create() validates the configuration against the extension's schema.json before calling createClient(), so extensions need no isset() checks of their own; an invalid configuration throws InvalidConfigurationException naming the field.
Health checks
ApiConfigurationHealthChecker serves the health route, app:api-configuration:health and application dashboards. For a type with a HealthProbeInterface (tagged automatically) it calls the probe with the decrypted configuration; otherwise it builds the client and calls getHealthInfo(). Types without a full client, such as schema-only connection types, only need a probe.
Security
Access
Every operation of the ApiConfiguration resource under /api/admin/api_configurations — reads and the health and authorize sub-resources included — requires ROLE_ADMIN, because configJson holds the credentials for the upstream systems.
Secrets in configJson
A type marks its secret keys in its own schema.json with the standard JSON Schema annotation writeOnly:
{
"properties": {
"username": { "type": "string" },
"password": { "type": "string", "writeOnly": true },
"oauth": {
"type": "object",
"properties": {
"client_secret": { "type": "string", "writeOnly": true }
}
}
}
}
Marked keys are found in nested objects, local $refs, allOf/anyOf/oneOf, if/then/else, array items and additionalProperties. The schema of a type is looked up by schema provider name, then by extension name, then by the type const of the registered schemas; for an unknown type the marked keys of all schemas apply, so a missing mapping over-masks instead of leaking.
For marked keys the bundle guarantees:
- Encrypted at rest. Values are stored as
enc:v1:<ciphertext>(libsodium secretbox viaDmstr\ApiPlatformUtils\Service\CredentialEncryption), whoever writes the entity — API, console or application code (DoctrineonFlushlistener). - Masked on read. API responses contain
********instead of the value, also for administrators. Logs of the configuration validator are masked the same way. - Kept on update. In
PUTandPATCH, an omitted key or the mask********keeps the stored value,nullremoves the key, any other value replaces it. Nothing is carried over whentypechanges. - Decrypted only for the client.
ApiClientFactory::create()andcreateFromEntity()decrypt before callingApiExtensionInterface::createClient(). Code that reads secrets fromApiConfiguration::getConfigJson()itself (for example anAuthorizableExtensionInterfacereading a client secret) must decrypt withDmstr\ApiConfiguration\Security\ConfigSecrets::decrypt().
Encryption key
The key comes from dmstr/api-platform-utils-bundle. CredentialEncryption expects the 32 raw key bytes, the generated key is base64, so decode it in the configuration:
# config/packages/dmstr_api_platform_utils.yaml dmstr_api_platform_utils: credential_encryption: key: '%env(base64:CREDENTIALS_ENCRYPTION_KEY)%'
bin/console dmstr:generate-encryption-key # prints CREDENTIALS_ENCRYPTION_KEY=...
Without a usable key (missing, wrong length, not base64), storing or using a secret fails with Dmstr\ApiConfiguration\Security\SecretEncryptionException; nothing is stored in clear. Configurations without secrets keep working. Changing the key makes existing secrets unreadable.
Existing rows
Rows stored before encryption at rest hold their secrets in clear. Encrypt them once after the upgrade; the command is idempotent and leaves encrypted values alone, so it can also run on every deployment:
bin/console app:api-configuration:encrypt-secrets --dry-run bin/console app:api-configuration:encrypt-secrets
Rows that are updated through Doctrine are encrypted on the next flush anyway.
License
MIT © diemeisterei GmbH