Skip to main content

keystore-credstore

Use this skill whenever migrating an SAP BTP Neo Java app that uses keystores, password storage, or any JNDI-bound credential lookup to Cloud Foundry. Triggers on `com.sap.cloud.crypto.keystore.api.KeyStoreService`, `com.sap.cloud.security.password.PasswordStorage`, `<resource-ref>` blocks for `KeyStoreService` or `PasswordStorage` in `web.xml`, `InitialContext.lookup("java:comp/env/KeyStoreService"|"PasswordStorage")`, or `@Resource(name = "KeyStoreService"|"PasswordStorage")`. Even if the user just says "migrate this app to CF" without naming credentials explicitly, invoke this skill the moment any of those symbols appear in the source. Replaces JNDI lookups with the SAP Credential Store REST API over mTLS, ships an offline-friendly Lombok-free Java client, and emits a Step 4 mtad that BINDS to a pre-existing, manually-provisioned credstore instance via `org.cloudfoundry.existing-service` (do NOT create or delete the credstore from the MTA).

소스 정보

저장소
SAP-samples/btp-neo-java-app-migration
최근 소스 활동
2026년 7월 29일 13:30
감지된 SKILL.md 언어
영어
스타
6
포크
0

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
9 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
keystore-credstore
description
Use this skill whenever migrating an SAP BTP Neo Java app that uses keystores, password storage, or any JNDI-bound credential lookup to Cloud Foundry. Triggers on `com.sap.cloud.crypto.keystore.api.KeyStoreService`, `com.sap.cloud.security.password.PasswordStorage`, `<resource-ref>` blocks for `KeyStoreService` or `PasswordStorage` in `web.xml`, `InitialContext.lookup("java:comp/env/KeyStoreService"|"PasswordStorage")`, or `@Resource(name = "KeyStoreService"|"PasswordStorage")`. Even if the user just says "migrate this app to CF" without naming credentials explicitly, invoke this skill the moment any of those symbols appear in the source. Replaces JNDI lookups with the SAP Credential Store REST API over mTLS, ships an offline-friendly Lombok-free Java client, and emits a Step 4 mtad that BINDS to a pre-existing, manually-provisioned credstore instance via `org.cloudfoundry.existing-service` (do NOT create or delete the credstore from the MTA).
disable-model-invocation
false
allowed-tools
Read, Edit, Write, Bash, Grep, Glob
# Keystore Service Migrate keystore and password storage to SAP Credential Store. ## Purpose Replace Neo's `KeyStoreService` and `PasswordStorage` JNDI resources with SAP Credential Store service using mTLS authentication and REST API. ## Detection This skill applies if any of these patterns are found: ### In web.xml ```xml <resource-ref> <res-ref-name>KeyStoreService</res-ref-name> <res-type>com.sap.cloud.crypto.keystore.api.KeyStoreService</res-type> </resource-ref> <!-- OR --> <resource-ref> <res-ref-name>PasswordStorage</res-ref-name> <res-type>com.sap.cloud.security.password.PasswordStorage</res-type> </resource-ref> ``` ### In Java source files ```java import com.sap.cloud.crypto.keystore.api.KeyStoreService; import com.sap.cloud.security.password.PasswordStorage; @Resource(name = "KeyStoreService") private KeyStoreService keyStoreService; @Resource(name = "PasswordStorage") private PasswordStorage passwordStorage; ``` ## Prerequisites > **Working directory:** This skill must run inside the `-cf-migration` copy of your app, created by `jakarta-java25-migration` or `neo-to-cf-migration-orchestrator`. If your current directory does not end in `-cf-migration`, switch to it before proceeding. Before invoking this skill, ensure you have invoked: 1. **sdk-replacement** - `Use the sdk-replacement skill` - Sets up SAP Cloud SDK - REQUIRED before this skill Also required: - Credential Store entitlement in subaccount ## Transformation Steps ### Step 1: Remove Resource References from web.xml **Remove these from web.xml:** ```xml <resource-ref> <res-ref-name>KeyStoreService</res-ref-name> <res-type>com.sap.cloud.crypto.keystore.api.KeyStoreService</res-type> </resource-ref> <resource-ref> <res-ref-name>PasswordStorage</res-ref-name> <res-type>com.sap.cloud.security.password.PasswordStorage</res-type> </resource-ref> ``` ### Step 2: Copy Client Classes Copy the following helper classes to your project from [assets/](assets/). All assets declare `package com.example.document;`, so place them flat under `src/main/java/com/example/document/`: > **Note on the package**: `com.example.document` is a placeholder. Rename to your project's package > (e.g. `com.acme.security`) when you copy the assets in, and update the imports below to match. | File | |------| | `ServiceCredentials.java` | | `ServiceCredentialsAccessor.java` | | `CertificateParser.java` | | `KeyParser.java` | | `SSLContextProvider.java` | | `CredStoreResponse.java` | | `CredStoreRequestBuilder.java` | | `CredStoreClient.java` | These classes handle: - Reading service credentials from VCAP_SERVICES via `DefaultServiceBindingAccessor` - Parsing PEM certificates (X.509) and PKCS#1 private keys (BouncyCastle PEMParser) - Creating an `SSLContext` for mTLS (PKCS12 keystore in memory) - REST API calls to Credential Store using `java.net.http.HttpClient` The classes have no Lombok dependency — getters are written by hand so they build cleanly under JDK 25. #### Required Maven dependencies Add these to your application's `pom.xml`. The `cf-tomcat-bom` + `sdk-modules-bom` BOMs already manage versions for `scp-cf` (which transitively brings in `service-binding-api`, BouncyCastle, Apache HttpClient, Jackson, and SLF4J), so you only need the `scp-cf` dependency itself plus `jakarta.servlet-api` for the servlet: ```xml <properties> <!-- sap_java_buildpack_jakarta supports SapMachine 17, 21, and 25. Always pin the runtime JRE explicitly via JBP_CONFIG_SAP_MACHINE_JRE in mtad.yaml / manifest.yml, and make sure this compile target matches that major version — a higher class file version yields java.lang.UnsupportedClassVersionError at servlet load time (HTTP 500). --> <maven.compiler.source>25</maven.compiler.source> <maven.compiler.target>25</maven.compiler.target> <!-- Resolve via sdk-replacement Step 3. --> <cf-tomcat-bom-version>RESOLVED_CF_TOMCAT_BOM_VERSION</cf-tomcat-bom-version> <sdk-modules-bom-version>RESOLVED_SDK_MODULES_BOM_VERSION</sdk-modules-bom-version> <jakarta.servlet-api.version>6.1.0</jakarta.servlet-api.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>com.sap.cloud.sjb.cf</groupId> <artifactId>cf-tomcat-bom</artifactId> <version>${cf-tomcat-bom-version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.sap.cloud.sdk</groupId> <artifactId>sdk-modules-bom</artifactId> <version>${sdk-modules-bom-version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>com.sap.cloud.sdk.cloudplatform</groupId> <artifactId>scp-cf</artifactId> </dependency> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>${jakarta.servlet-api.version}</version> <scope>provided</scope> </dependency> </dependencies> ``` > **Pin the runtime JRE explicitly.** `sap_java_buildpack_jakarta` supports SapMachine > 17, 21, and 25. The buildpack's implicit JRE choice changes over time, so don't rely on > it — always pin the version in `mtad.yaml` (or `manifest.yml`) and keep the compile > target in `pom.xml` aligned with it. A class file newer than the runtime JRE causes > Tomcat to fail loading the servlet with > `java.lang.UnsupportedClassVersionError: ... class file version N.0, this version of the > Java Runtime only recognizes class file versions up to M.0` and every request returns > HTTP 500. > > For Java 25 (the recommended target for new migrations) add this to your module > `properties:` block in `mtad.yaml`: > > ```yaml > JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jre.SAPMachineJRE']" > JBP_CONFIG_SAP_MACHINE_JRE: '{ version: 25.+ }' > ``` > > Same form for any other supported major version — substitute `17.+` or `21.+`. > Build locally with a matching JDK (e.g. SapMachine 25 from https://sapmachine.io/). > The Lombok-free helper classes in `assets/` compile cleanly on any supported version. ### Step 3: Update Application Code > **The HTTP URL contract changes — do NOT preserve the old Neo query parameters.** > > The Neo `KeyStoreServlet` was driven by params shaped around its JNDI API: > `?method=getKeyStore&keyStoreName=<file>&password=<password>`. None of those map > onto SAP Credential Store, which is namespace-scoped and addresses each credential > by alias. The migrated servlet **MUST** accept the new contract: > > | URL | Behaviour | > |-----|-----------| > | `GET /keystore?namespace=<ns>` | List all credentials in `<ns>` (calls `CredStoreClient.retrieveCredentials(ns)`) | > | `GET /keystore?namespace=<ns>&alias=<name>` | Retrieve a single credential by alias (calls `CredStoreClient.retrieveCredential(alias, ns)`) | > | `GET /keystore` (no `namespace`) | Reject with `400 Bad Request`, body `Namespace is required!` | > > A Neo password-storage app likewise drops to: `GET /?namespace=<ns>&alias=<name>` > calling `CredStoreClient.retrievePassword(alias, ns)`. > > Why this matters: do **NOT** carry over the `method=getKeyStore` / > `keyStoreName=` / `password=` validation block from the original Neo servlet. > Doing so makes the migrated app keep returning `400` for the actual contract a > credstore-aware caller will use, and silently breaks integration tests. Replace > the old guard with the namespace check shown below. **Before (Neo `KeyStoreServlet` — JNDI lookup, `method=…&keyStoreName=…&password=…`):** ```java import com.sap.cloud.crypto.keystore.api.KeyStoreService; import java.security.KeyStore; import javax.naming.InitialContext; import javax.servlet.http.*; public class KeyStoreServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String method = request.getParameter("method"); if (method == null || !"getKeyStore".equals(method)) { // Neo-shaped guard. DELETE THIS BLOCK in the migrated version — // the new contract does not use a "method" parameter. response.sendError(HttpServletResponse.SC_BAD_REQUEST, "method invalid"); return; } KeyStoreService svc = (KeyStoreService) new InitialContext().lookup("java:comp/env/KeyStoreService"); KeyStore keyStore = svc.getKeyStore( request.getParameter("keyStoreName"), request.getParameter("password").toCharArray()); // ... iterate keyStore.aliases() ... } } ``` **After (Cloud Foundry — `?namespace=<ns>[&alias=<name>]`):** ```java package com.example.document; // CredStoreClient and CredStoreResponse live in the same package — no import needed. import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.security.GeneralSecurityException; public class KeyStoreServlet extends HttpServlet { private static final String PARAM_ALIAS = "alias"; private static final String PARAM_NAMESPACE = "namespace"; private final CredStoreClient credStoreClient; public KeyStoreServlet() throws GeneralSecurityException, IOException { this.credStoreClient = new CredStoreClient(); } @Override public void destroy() { // Release the mTLS private key when the servlet is taken out of service. // CredStoreClient holds the key for its whole lifetime (the SSLContext // reuses it across requests), so it must only be destroyed here — never // per request — otherwise subsequent mTLS handshakes fail. credStoreClient.close(); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { String alias = request.getParameter(PARAM_ALIAS); String namespace = request.getParameter(PARAM_NAMESPACE); if (isBlank(namespace)) { sendError(response, "Namespace is required!", HttpServletResponse.SC_BAD_REQUEST); return; } CredStoreResponse credstoreResponse = isBlank(alias) ? credStoreClient.retrieveCredentials(namespace) // list all keys : credStoreClient.retrieveCredential(alias, namespace); // single key if (credstoreResponse.isSuccess()) { sendOk(response, isBlank(alias) ? "Successfully retrieved credentials." : "Successfully retrieved credential."); } else { sendError(response, credstoreResponse.getMessage(), HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } private static boolean isBlank(String s) { return s == null || s.isBlank(); } private static void sendOk(HttpServletResponse response, String body) throws IOException { try (PrintWriter out = response.getWriter()) { out.println(body); } } private static void sendError(HttpServletResponse response, String body, int status) throws IOException { response.setStatus(status); try (PrintWriter out = response.getWriter()) { out.println(body); } } } ``` The corresponding `web.xml` must drop the `<resource-ref>` (Step 1) AND map the servlet at `/keystore` (or `/` for password apps) so the URL above resolves. Any Neo `ErrorUtil` helper that imported `javax.servlet.*` must be ported to `jakarta.servlet.*` or inlined as private helpers like above. **Before (Neo Password Storage — JNDI lookup, no namespace param):** ```java import com.sap.cloud.security.password.PasswordStorage; @Resource(name = "PasswordStorage") private PasswordStorage passwordStorage; public void usePassword(HttpServletRequest request) throws Exception { String alias = request.getParameter("alias"); // alias only char[] password = passwordStorage.getPassword(alias); // ... use password ... } ``` **After (Cloud Foundry — `?namespace=<ns>&alias=<name>`):** ```java @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { String alias = request.getParameter("alias"); String namespace = request.getParameter("namespace"); // NEW required param if (isBlank(alias) || isBlank(namespace)) { response.getWriter().println("Alias and namespace must be provided as query parameters."); return; } CredStoreResponse r = credStoreClient.retrievePassword(alias, namespace); response.getWriter().println(r.isSuccess() ? "Password retrieved successfully." : "Failed to retrieve password."); } ``` ### Step 4: Update MTA Descriptor > 🛑 **HARD RULE — the credstore resource MUST be `org.cloudfoundry.existing-service`.** > Do **not** emit `type: org.cloudfoundry.managed-service` for credstore, and do > **not** add a `parameters.service: credstore` / `service-plan:` block. Getting > this wrong is the single most common failure of this migration: > > | If you emit… | What happens at `cf deploy` | > |---|---| > | `managed-service` (`service: credstore`) | Broker rejects it — `Service broker credstore failed with: Illegal parameters or arguments used` — because the mTLS `config` is set out-of-band, not in the MTA. Deploy fails. | > | `existing-service` ✅ | Binds to the operator-provisioned instance. Correct. | > > The credstore instance is created and mTLS-configured **outside** the MTA > (by the space operator, or by CI before deploy). The MTA only *binds* to it. ```yaml modules: - name: ${app-name} type: java.tomcat path: target/<artifactId>.war parameters:
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기