Problem
src/Entity/ApiConfiguration.php (master, unchanged as of 2026-08-23) restricts every write operation to ROLE_ADMIN, but leaves reads at ROLE_USER:
routePrefix: '/admin',
security: "is_granted('ROLE_USER')",
configJson is a plain readable property — no normalizationContext groups are declared on the resource, and ApiProperty defaults to readable: true:
#[ORM\Column(name: 'config_json', type: Types::JSON)]
#[JsonSchema]
#[Assert\NotBlank]
#[ApiConfigurationConstraint]
#[ApiProperty(description: 'API configuration object. Must match one of the supported API types.')]
private array $configJson = [];
configJson is exactly where credentials for the upstream system live — the field's own JSON schema models auth_type: basic|bearer with username/password or token. So any authenticated user can read the credentials of every configured upstream API via GET /api/admin/api_configurations.
In our case one configuration holds HTTP basic credentials for a workflow engine whose REST API can deploy processes and mutate process state, and another holds a bearer token for an upstream data service. Any user who can obtain a token for the application — including the lowest-privileged domain role — can read both and then talk to those systems directly, bypassing the application entirely.
This is not merely a matter of degree versus the Job-resource case in the sibling bundle: read access to a credential store is a privilege escalation, not an information leak.
Why this looks like an oversight
The write operations were deliberately narrowed to ROLE_ADMIN (Post, Put, Patch, Delete, and the …/authorize sub-resource), and the resource sits behind routePrefix: '/admin' with openapi: new Operation(tags: ['System']). Everything about the resource says "operations only" except the read default. The sibling bundles that expose operational resources under /admin (doctrine-audit-log-bundle → LogEntry, symfony-system-resources-bundle → DoctrineMigrationVersion / MessengerMessage) all default to ROLE_ADMIN.
Proposal
Both parts, not either/or:
- Resource default →
ROLE_ADMIN (security: "is_granted('ROLE_ADMIN')"), matching the write operations and the sibling bundles. The …/health sub-resource can stay broader if consumers rely on it for status displays — it exposes no secrets.
- Never serialize secrets, regardless of the role. Even for an admin, returning
password / token over the API is more than the resource needs. Options, roughly in order of preference:
- redact known credential keys in a normalizer/output DTO (
password, token, secret, *_secret) so the shape stays intact but values are masked;
- or
#[ApiProperty(security: "is_granted('ROLE_ADMIN')")] on configJson as a minimum step;
- long term, keep secrets out of the row entirely (env/secret-store reference in
configJson, resolved server-side). We already do this in one application for a client secret specifically because configJson is API-readable — it would be good if the bundle made that the default rather than something each consumer has to remember.
Impact
Breaking for consumers whose non-admin clients read the configuration list (e.g. to discover a base URL). Suggest a config option or a reduced read projection for that use case rather than keeping the current default.
Notes for the fix
- Regression tests worth having: authenticated non-admin gets
403 on collection and item; an admin read does not contain the credential values.
- Reproduction is straightforward: seed a configuration with
auth_type: basic, then GET /api/admin/api_configurations with a token that carries only the baseline authenticated role.
Reported with AI assistance (Claude Code).
Problem
src/Entity/ApiConfiguration.php(master, unchanged as of 2026-08-23) restricts every write operation toROLE_ADMIN, but leaves reads atROLE_USER:configJsonis a plain readable property — nonormalizationContextgroups are declared on the resource, andApiPropertydefaults toreadable: true:configJsonis exactly where credentials for the upstream system live — the field's own JSON schema modelsauth_type: basic|bearerwithusername/passwordortoken. So any authenticated user can read the credentials of every configured upstream API viaGET /api/admin/api_configurations.In our case one configuration holds HTTP basic credentials for a workflow engine whose REST API can deploy processes and mutate process state, and another holds a bearer token for an upstream data service. Any user who can obtain a token for the application — including the lowest-privileged domain role — can read both and then talk to those systems directly, bypassing the application entirely.
This is not merely a matter of degree versus the
Job-resource case in the sibling bundle: read access to a credential store is a privilege escalation, not an information leak.Why this looks like an oversight
The write operations were deliberately narrowed to
ROLE_ADMIN(Post,Put,Patch,Delete, and the…/authorizesub-resource), and the resource sits behindroutePrefix: '/admin'withopenapi: new Operation(tags: ['System']). Everything about the resource says "operations only" except the read default. The sibling bundles that expose operational resources under/admin(doctrine-audit-log-bundle→LogEntry,symfony-system-resources-bundle→DoctrineMigrationVersion/MessengerMessage) all default toROLE_ADMIN.Proposal
Both parts, not either/or:
ROLE_ADMIN(security: "is_granted('ROLE_ADMIN')"), matching the write operations and the sibling bundles. The…/healthsub-resource can stay broader if consumers rely on it for status displays — it exposes no secrets.password/tokenover the API is more than the resource needs. Options, roughly in order of preference:password,token,secret,*_secret) so the shape stays intact but values are masked;#[ApiProperty(security: "is_granted('ROLE_ADMIN')")]onconfigJsonas a minimum step;configJson, resolved server-side). We already do this in one application for a client secret specifically becauseconfigJsonis API-readable — it would be good if the bundle made that the default rather than something each consumer has to remember.Impact
Breaking for consumers whose non-admin clients read the configuration list (e.g. to discover a base URL). Suggest a config option or a reduced read projection for that use case rather than keeping the current default.
Notes for the fix
403on collection and item; an admin read does not contain the credential values.auth_type: basic, thenGET /api/admin/api_configurationswith a token that carries only the baseline authenticated role.Reported with AI assistance (Claude Code).