bmad-method-quarkus 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/bmm-skills/agents/bmad-quarkus-build/SKILL.md +7 -3
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-error-handling-i18n/SKILL.md +6 -2
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-hexagonal-core/SKILL.md +64 -17
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-kafka-messaging/SKILL.md +5 -3
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-observability-otel/SKILL.md +191 -13
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-openapi-tmforum/SKILL.md +6 -7
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-security-standards/SKILL.md +132 -0
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-sql-jdbc-agroal/SKILL.md +5 -2
- package/src/bmm-skills/agents/bmad-quarkus-build/skills/quarkus-temporal-workflows/SKILL.md +616 -0
- package/src/commands/quarkus-all.md +6 -5
|
@@ -31,7 +31,7 @@ The raw OpenAPI document at `/q/openapi` may stay enabled in prod behind the gat
|
|
|
31
31
|
|
|
32
32
|
### URLs & versioning
|
|
33
33
|
- Plural kebab-free camelCase resource names as in TMF specs: `/user`, `/partyAccount` — follow the TMF spec exactly when implementing one; plural nouns for custom resources.
|
|
34
|
-
- Base path starts with the module's semantic name (`iam`, `tenant` — see quarkus-hexagonal-core skill, never a `bcNN` code): `/{module}/{context}/{apiName}/v{major}` e.g. `/iam/tmf-api/digitalIdentityManagement/v4`. Major version in path only.
|
|
34
|
+
- Base path starts with the module's semantic name (`iam`, `tenant` — see quarkus-hexagonal-core skill, never a `bcNN` code): `/{module}/{context}/{apiName}/v{major}` e.g. `/iam/tmf-api/digitalIdentityManagement/v4`. Major version in path only. That leading `/{module}` segment is also the app's **API gateway base path**, and it is declared in the same PR in README §2 and `service.yaml` under `metadata.gateway` — infra configures the route from there (see quarkus-hexagonal-core skill, "Gateway routing").
|
|
35
35
|
|
|
36
36
|
### Standard operations
|
|
37
37
|
| Operation | Verb | Response |
|
|
@@ -89,6 +89,7 @@ public class DigitalIdentityResource {
|
|
|
89
89
|
|
|
90
90
|
@Inject CreateDigitalIdentityHandler createHandler; // the slice's business logic
|
|
91
91
|
@Inject FindDigitalIdentityHandler findHandler;
|
|
92
|
+
@Inject JsonWebToken jwt; // tenant/roles come from the validated token
|
|
92
93
|
|
|
93
94
|
@GET
|
|
94
95
|
@Blocking
|
|
@@ -96,12 +97,11 @@ public class DigitalIdentityResource {
|
|
|
96
97
|
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = DigitalIdentityDto[].class)))
|
|
97
98
|
@APIResponse(responseCode = "400", ref = "#/components/responses/BadRequest")
|
|
98
99
|
public RestResponse<List<DigitalIdentityDto>> list(
|
|
99
|
-
@HeaderParam("tenantId") String tenantId,
|
|
100
100
|
@QueryParam("fields") String fields,
|
|
101
101
|
@QueryParam("offset") @DefaultValue("0") int offset,
|
|
102
102
|
@QueryParam("limit") @DefaultValue("20") int limit) {
|
|
103
103
|
var query = FilterDigitalIdentityDto.builder()
|
|
104
|
-
.tenantId(
|
|
104
|
+
.tenantId(jwt.getClaim("tenant_id")).fields(fields).offset(offset).limit(limit).build();
|
|
105
105
|
return RestResponse.ok(findHandler.process(query).getItems());
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -109,10 +109,9 @@ public class DigitalIdentityResource {
|
|
|
109
109
|
@Blocking
|
|
110
110
|
@Operation(operationId = "createDigitalIdentity")
|
|
111
111
|
@APIResponse(responseCode = "201", headers = @Header(name = "Location"))
|
|
112
|
-
public RestResponse<DigitalIdentityDto> create(@
|
|
113
|
-
@Valid DigitalIdentityCreateDto dto,
|
|
112
|
+
public RestResponse<DigitalIdentityDto> create(@Valid DigitalIdentityCreateDto dto,
|
|
114
113
|
@Context UriInfo uri) {
|
|
115
|
-
dto.setTenantId(
|
|
114
|
+
dto.setTenantId(jwt.getClaim("tenant_id")); // validated JWT claim, never a header
|
|
116
115
|
var created = createHandler.process(dto); // BusinessException propagates to the global handler
|
|
117
116
|
return RestResponse.ResponseBuilder
|
|
118
117
|
.created(uri.getAbsolutePathBuilder().path(created.getId()).build())
|
|
@@ -124,7 +123,7 @@ public class DigitalIdentityResource {
|
|
|
124
123
|
Rules:
|
|
125
124
|
- Return `RestResponse<T>` synchronously — typed, so the generated schema is right. Never `Uni`/`Multi`: the `Handler` is blocking JDBC.
|
|
126
125
|
- `@Blocking` (`io.smallrye.common.annotation.Blocking`) on JDBC-backed methods. With a plain return type Quarkus REST already dispatches to a worker thread, so it is redundant today — keep it as an explicit threading contract that survives a later signature change. `@RunOnVirtualThread` is the high-concurrency alternative (see sql skill §8).
|
|
127
|
-
-
|
|
126
|
+
- Take `tenantId` — and anything else that drives authorization, such as `partyId` or `partyRolList` — from the validated token (`@Inject JsonWebToken jwt` → `jwt.getClaim(...)`, or `@Claim`; `SecurityIdentity` carries roles and mechanism attributes, **not** claims), **never** from a `@HeaderParam`: a header the caller sets is not authentication (see quarkus-security-standards skill §4 and the tenancy rule in quarkus-hexagonal-core). `language` and other non-security metadata may come from headers. Set them on the request DTO before calling `process()`.
|
|
128
127
|
- No `try/catch` around `process()`. Errors travel as `BusinessException` to `GlobalExceptionHandler`, which resolves the localized TMF Error body (see quarkus-error-handling-i18n skill).
|
|
129
128
|
- `operationId` on every operation (client generation depends on it); match TMF naming (`listX`, `retrieveX`, `createX`, `patchX`, `deleteX`).
|
|
130
129
|
- Reusable components: define common responses (400/401/404/409/500 with TMF Error schema) once via an `@OpenAPIDefinition`/filter class, `ref` them everywhere.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: quarkus-security-standards
|
|
3
|
+
description: Security baseline for Quarkus backend services — credential and environment-variable handling via `.env` (never committed) plus a committed `.env.example` template, secrets management, authentication/authorization (OIDC/JWT), transport security, input/output hardening, dependency and container hardening, and secret-safe logging. Use this skill whenever the user creates or reviews `application.properties`/`.env` files, injects a credential/URL/API key/token, adds `quarkus-oidc`/`quarkus-smallrye-jwt`, configures CORS/TLS/headers, writes a `Dockerfile`, adds a dependency, or asks about security review, secret leakage, `.gitignore`, or OWASP. Pairs with quarkus-hexagonal-core (project layout), quarkus-sql-jdbc-agroal (injection-safe SQL), and quarkus-observability-otel (PII-safe logging).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Security Standard (Quarkus)
|
|
7
|
+
|
|
8
|
+
Applies to any Quarkus backend project; project directives (CLAUDE.md, ADRs, explicit instructions) override these defaults where they conflict.
|
|
9
|
+
|
|
10
|
+
## 1. Environment variables: `.env` for local secrets, never committed
|
|
11
|
+
|
|
12
|
+
**No credential, URL, API key, token, or other environment-specific secret is ever hardcoded in source or committed to the repository.** Locally, they live in a `.env` file at the project root; in every other environment they come from the platform's secret store (see §2). This is the first rule to apply on any new service, before the first `application.properties` line is written.
|
|
13
|
+
|
|
14
|
+
Rules, in order:
|
|
15
|
+
|
|
16
|
+
1. **Create `.env`** at the project root (or per-app root in a monorepo, `apps/<app>/.env`) holding every local credential, connection URL, host, port, and API key the service needs to run in `%dev`/`%test`. Real values only, never sample/placeholder values here.
|
|
17
|
+
2. **`.env` is never versioned.** Add it to `.gitignore` **immediately**, in the same commit that creates it — never after the fact:
|
|
18
|
+
|
|
19
|
+
```gitignore
|
|
20
|
+
# Local environment secrets — never commit
|
|
21
|
+
.env
|
|
22
|
+
.env.*
|
|
23
|
+
!.env.example
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If `.env` was ever committed by mistake, it must be purged from history (`git filter-repo` or equivalent) and every credential it contained rotated — removing the file from the tip of the branch is not sufficient.
|
|
27
|
+
3. **Create `.env.example`** alongside it, committed to the repo, listing every variable `.env` must define — **keys only, placeholder or empty values, never real secrets**:
|
|
28
|
+
|
|
29
|
+
```dotenv
|
|
30
|
+
# .env.example — copy to .env and fill in real values. Never commit .env.
|
|
31
|
+
DB_HOST=localhost
|
|
32
|
+
DB_PORT=5432
|
|
33
|
+
DB_NAME=customer
|
|
34
|
+
DB_USER=
|
|
35
|
+
DB_PASSWORD=
|
|
36
|
+
KAFKA_BOOTSTRAP_SERVERS=localhost:9092
|
|
37
|
+
OIDC_AUTH_SERVER_URL=
|
|
38
|
+
OIDC_CLIENT_ID=
|
|
39
|
+
OIDC_CLIENT_SECRET=
|
|
40
|
+
EXTERNAL_API_BASE_URL=
|
|
41
|
+
EXTERNAL_API_KEY=
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`.env.example` is the authoritative, reviewable list of what the service needs — update it in the same PR that introduces a new variable. A variable missing from `.env.example` does not exist as far as onboarding and CI are concerned.
|
|
45
|
+
4. **Reference the variables from `application.properties` via Quarkus property expressions**, never by reading `.env` manually in code:
|
|
46
|
+
|
|
47
|
+
```properties
|
|
48
|
+
quarkus.datasource.username=${DB_USER}
|
|
49
|
+
quarkus.datasource.password=${DB_PASSWORD}
|
|
50
|
+
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}
|
|
51
|
+
quarkus.oidc.auth-server-url=${OIDC_AUTH_SERVER_URL}
|
|
52
|
+
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Quarkus resolves `${VAR}` from process environment variables at runtime (uppercase-with-underscores env var names map automatically — no extra wiring). Quarkus/SmallRye Config reads a `.env` file in the working directory natively in dev and test — no extension is needed, and `quarkus-dotenv` should not be added. Nothing in `main` code parses `.env` directly.
|
|
56
|
+
5. **Never print, log, or return an environment variable's value.** A `Handler`/`Resource` that echoes a config value back for "debugging" is a leak waiting to happen — see §7.
|
|
57
|
+
|
|
58
|
+
This applies to **every** environment-specific value, not just passwords: hostnames, ports, bucket names, tenant-specific URLs, and feature-flag endpoints belong in `.env`/the platform secret store too — hardcoding a "just a hostname" value is how an internal URL ends up in a public repo.
|
|
59
|
+
|
|
60
|
+
## 2. Beyond local dev: secrets in real environments
|
|
61
|
+
|
|
62
|
+
`.env` is a **local-development convenience only** — it is never the secret source in a deployed environment.
|
|
63
|
+
|
|
64
|
+
- Deployed environments (dev cluster, staging, prod) inject secrets as environment variables or mounted files from a vault (HashiCorp Vault, cloud KMS/Secrets Manager, Kubernetes `Secret` backed by an external secret operator) — never from a `.env` file baked into an image.
|
|
65
|
+
- `application.properties` stays identical across environments: `${VAR}` placeholders only. What changes per environment is *where the platform gets the value from*, never the property file.
|
|
66
|
+
- Kubernetes: mount secrets as env vars via `secretKeyRef`, never as plain `ConfigMap` values, and never bake a secret into the container image at build time.
|
|
67
|
+
- The per-app `README.md` **Secrets** section (see quarkus-hexagonal-core skill, §"Per-app documentation") documents logical name, purpose, vault source, owner, and rotation period for every secret — **never the value itself**.
|
|
68
|
+
- Rotate credentials on a schedule and immediately on suspected exposure (accidental commit, departing team member with access, log leak).
|
|
69
|
+
|
|
70
|
+
## 3. No secrets in code, config, or version control
|
|
71
|
+
|
|
72
|
+
- No API key, password, connection string, private key, or token as a string literal anywhere in `src/`, `pom.xml`, test fixtures, or committed `application.properties`. Grep for this in review: `grep -rniE "(password|secret|api[_-]?key|token)[[:space:]]*=[[:space:]]*['\"][^$]" src/` (a `${VAR}` right-hand side is fine; a literal value is not).
|
|
73
|
+
- Test fixtures use `%test` profile values pointing at Dev Services (ephemeral Testcontainers credentials, not real ones) — never a copy of a real credential "just for tests."
|
|
74
|
+
- `application.properties` committed to the repo may reference `${VAR}` freely; it never assigns a literal secret value, including in a profile-specific override (`%prod.quarkus.datasource.password=...` is banned exactly like the unscoped form).
|
|
75
|
+
- Private keys/certificates (`.pem`, `.jks`, `.p12`) are never committed. Reference their filesystem path (mounted from a secret) via `${VAR}`, same as any other credential.
|
|
76
|
+
|
|
77
|
+
## 4. Authentication and authorization
|
|
78
|
+
|
|
79
|
+
- Inbound REST/gRPC authentication is `quarkus-oidc` (OIDC/JWT) against the org's identity provider — no home-grown token schemes, no hardcoded API keys as the sole auth mechanism for a production endpoint.
|
|
80
|
+
- `tenantId` is read from a **validated JWT claim**, never a free-form request header (see quarkus-hexagonal-core skill's tenancy rule) — a header the caller can set to any value is not authentication.
|
|
81
|
+
- Authorize at the `Resource`/`GrpcService` boundary with `@RolesAllowed`/`SecurityIdentity` checks before invoking `Handler.process()`; the `Handler` trusts the DTO it receives came from an already-authorized caller and does not re-implement authorization logic.
|
|
82
|
+
- Service-to-service calls (internal gRPC) use mTLS or a service-account token from the platform's identity system, injected via `${VAR}` per §1 — never a shared static secret checked into `common/client`.
|
|
83
|
+
- `quarkus.oidc.credentials.secret` and any client secret follow §1/§3 exactly: `.env` locally, vault in deployed environments, `${VAR}` in properties.
|
|
84
|
+
|
|
85
|
+
## 5. Transport and network security
|
|
86
|
+
|
|
87
|
+
- TLS terminates at the ingress/service mesh in most deployments; where the service terminates TLS itself, `quarkus.http.ssl.certificate.*` paths point at mounted secret files via `${VAR}`, never inline certs/keys.
|
|
88
|
+
- HTTP is never the accepted transport for a real environment. **Where TLS terminates at the ingress (the usual case) leave `quarkus.http.insecure-requests` alone** — the pod has no certificate, so forcing `redirect`/`disabled` there breaks its own probes and the mesh's plain-HTTP hop to it. Set it only on a service that terminates TLS itself; otherwise enforce HTTPS at the ingress, where the certificate lives.
|
|
89
|
+
- CORS is explicit and scoped, never `*` in a deployed environment:
|
|
90
|
+
|
|
91
|
+
```properties
|
|
92
|
+
quarkus.http.cors.enabled=true
|
|
93
|
+
quarkus.http.cors.origins=${ALLOWED_ORIGINS:http://localhost:3000}
|
|
94
|
+
quarkus.http.cors.methods=GET,POST,PUT,PATCH,DELETE
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- Internal service calls are gRPC, external/north-bound is REST (see quarkus-hexagonal-core skill) — this also means the internal surface is not exposed to the public network by default; ingress rules should reflect that boundary.
|
|
98
|
+
|
|
99
|
+
## 6. Input handling and injection
|
|
100
|
+
|
|
101
|
+
- SQL: `PreparedStatement` placeholders only, never string-concatenated user input — full standard in quarkus-sql-jdbc-agroal skill (§2 there). This skill's `.env` rule and that skill's placeholder rule are the two halves of "never trust a string that reaches persistence."
|
|
102
|
+
- Bean Validation (`@NotBlank`, `@Size`, `@Pattern`) on every DTO field at the transport boundary; `Handler.validate()` covers business-rule validation (see quarkus-hexagonal-core skill) — reject malformed input before it reaches `execution()`.
|
|
103
|
+
- Deserialize only into typed DTOs (`@RegisterForReflection` where native requires it) — never accept and reflect back arbitrary JSON structures (mass-assignment risk).
|
|
104
|
+
- Any value echoed into a response, log line, or downstream call that originated from user input is treated as untrusted: encode/escape for its destination context (JSON, log line, SQL, shell) rather than assuming it is already safe.
|
|
105
|
+
- Never build a shell command, file path, or SQL fragment by concatenating request data. If a slice genuinely needs to invoke an external process, pass arguments as an array (no shell interpretation), never a single interpolated string.
|
|
106
|
+
|
|
107
|
+
## 7. Secret-safe logging and error responses
|
|
108
|
+
|
|
109
|
+
- **Never log a credential, token, full connection string, or environment-variable value.** Log the variable's *name*, never its value, when debugging config issues.
|
|
110
|
+
- Pairs with quarkus-observability-otel skill: spans, attributes, and audit `context` are business-relevant and low-cardinality — the same rule bans PII, and it bans secrets identically. If a field would be a problem in a trace, it is a problem in a log line too.
|
|
111
|
+
- `GlobalExceptionHandler`/`GrpcExceptionInterceptor` (see quarkus-error-handling-i18n skill) return the catalog message only — never a stack trace, an internal exception message, or a raw `SQLException` string to the client. Full detail goes to the log (still secret-scrubbed), the client gets `<MOD>-<HTTP>-<seq>` + localized text + `X-Trace-Id`.
|
|
112
|
+
- Actuator/health/metrics endpoints (`/q/health`, `/q/metrics`) never surface configuration values, environment variables, or datasource URLs — verify no custom health check echoes a connection string in its response.
|
|
113
|
+
|
|
114
|
+
## 8. Dependencies and container hardening
|
|
115
|
+
|
|
116
|
+
- Run dependency vulnerability scanning (OWASP Dependency-Check, `mvn org.owasp:dependency-check-maven:check`, or the platform's equivalent) in CI; a new dependency with a known critical CVE and no fix available needs an explicit, documented exception, not a silent merge.
|
|
117
|
+
- Pin dependency versions through the Quarkus BOM; avoid version ranges that can silently pull in an unreviewed transitive update.
|
|
118
|
+
- The native-image Dockerfile (see quarkus-hexagonal-core skill) already enforces the two container-hardening rules that matter most: a minimal base image with **no JDK/JRE layer** in the final image, and a **non-root `USER`**. Never widen either for convenience.
|
|
119
|
+
- No secret is ever baked into a Docker image layer (`ENV MY_SECRET=...` in a `Dockerfile`, or a `COPY .env`) — secrets are injected at container start by the orchestrator, per §2.
|
|
120
|
+
|
|
121
|
+
## Checklist for a new service or slice
|
|
122
|
+
|
|
123
|
+
1. `.env` created with real local values; `.gitignore` entry added in the **same commit**.
|
|
124
|
+
2. `.env.example` committed with every variable name `.env` defines, placeholder/empty values only, kept current with every new variable.
|
|
125
|
+
3. `application.properties` references every credential/URL via `${VAR}` — zero literal secrets, in any profile.
|
|
126
|
+
4. `grep -rniE "(password|secret|api[_-]?key|token)[[:space:]]*=[[:space:]]*['\"][^$]" src/` returns nothing.
|
|
127
|
+
5. Deployed environments source the same variables from the platform vault/secret store, documented (never valued) in the app's `README.md` Secrets section.
|
|
128
|
+
6. Inbound auth is `quarkus-oidc`/JWT; `tenantId` comes from a validated claim, not a free header.
|
|
129
|
+
7. TLS enforced outside `%dev`; CORS origins explicit, never `*` in a deployed environment.
|
|
130
|
+
8. No log line, error response, health/metrics endpoint, or trace attribute ever carries a secret, token, or full connection string.
|
|
131
|
+
9. Native Dockerfile: non-root user, no JDK/JRE layer, no secret baked into a layer.
|
|
132
|
+
10. Dependency scan clean or exceptions documented.
|
|
@@ -207,7 +207,7 @@ Read side: `rs.getString("context")` then parse. Index jsonb lookups you actuall
|
|
|
207
207
|
|
|
208
208
|
## 6. Cross-cutting tables get a cross-cutting `Sql`
|
|
209
209
|
|
|
210
|
-
One `Sql` class per slice is the rule for the slice's **own** tables. A table that every slice writes the same way — `
|
|
210
|
+
One `Sql` class per slice is the rule for the slice's **own** tables. A table that every slice writes the same way — `outbox_event`, `audit_event`, `processed_event` — gets **one** `Sql` class in `common/`, and a `Connection`-first capability bean in front of it:
|
|
211
211
|
|
|
212
212
|
| Table | Owner in `common/` | Called by |
|
|
213
213
|
|---|---|---|
|
|
@@ -284,6 +284,9 @@ quarkus.flyway.migrate-at-start=false
|
|
|
284
284
|
%test.quarkus.flyway.enabled=true
|
|
285
285
|
%test.quarkus.flyway.migrate-at-start=true
|
|
286
286
|
|
|
287
|
+
# Classpath location by default. The monorepo keeps migrations in db/ at the REPO root, which is
|
|
288
|
+
# not on the classpath — either copy them into src/main/resources/db/migration at build time, or
|
|
289
|
+
# point Flyway at the filesystem: quarkus.flyway.locations=filesystem:../../db/migration
|
|
287
290
|
quarkus.flyway.locations=db/migration
|
|
288
291
|
quarkus.flyway.baseline-on-migrate=true # adopting an existing database
|
|
289
292
|
```
|
|
@@ -319,4 +322,4 @@ Rules:
|
|
|
319
322
|
5. `executeUpdate()` count returned or checked; `SQLException` propagated for the `Handler` to translate, never swallowed.
|
|
320
323
|
6. Generated ids via `RETURNING`; jsonb via `PGobject`; batch + chunking for bulk; keyset pagination if deep.
|
|
321
324
|
7. Rows mapped by hand into the slice's `*Dto`; Dev Services test written.
|
|
322
|
-
8. The table belongs to this slice. If it is cross-cutting (`
|
|
325
|
+
8. The table belongs to this slice. If it is cross-cutting (`outbox_event`, `audit_event`, `processed_event`), the method belongs in `common/` (§6) — not here.
|