diff --git a/.cdsrc.json b/.cdsrc.json index fbe934751..f85aeadcd 100644 --- a/.cdsrc.json +++ b/.cdsrc.json @@ -62,6 +62,14 @@ "author": { "roles": ["Tutorial.Author", "authenticated-user"] }, + "idpcustom": { + "roles": ["authenticated-user"], + "attr": { "origin": "sap.custom" } + }, + "idpdefault": { + "roles": ["authenticated-user"], + "attr": { "origin": "sap.default" } + }, "apiuser": { "password": "apikey", "roles": ["Tutorial.API", "authenticated-user"] diff --git a/.deploy/mta.yaml b/.deploy/mta.yaml index 439025737..704a2d421 100644 --- a/.deploy/mta.yaml +++ b/.deploy/mta.yaml @@ -10,7 +10,7 @@ ID: tutorials-ims # Bump this on each release you deploy — it's the version shown by `cf mtas` # and in the mtar filename (tutorials-ims_.mtar). Deploy is manual: # `cd .deploy && mbt build && cf deploy mta_archives/tutorials-ims_.mtar -e ../deploy/.mtaext -f`. -version: 1.33.0 +version: 1.34.0 # Top-level parameters (overridable per-env via deploy/.mtaext). parameters: @@ -359,16 +359,20 @@ modules: srv-mcp-url: ${srv-mcp-url} requires: - name: tutorials-xsuaa - # NOTE: the approuter is deliberately bound to ONLY the confidential - # tutorials-xsuaa (its login client). The PUBLIC tutorials-mcp issuer that - # .well-known OAuth discovery advertises is supplied as NON-SECRET config - # env (XSUAA_MCP_URL + XSUAA_MCP_XSAPPNAME, see properties below) — NOT a - # 2nd xsuaa binding. Binding a 2nd xsuaa would make @sap/approuter's own - # login handshake pick a binding non-deterministically (no route selector), - # risking 500s on every xsuaa-authed route. Keeping one binding avoids that. + # Dual auth binding (approuter 23.x). XSUAA is kept for authorization/scopes; + # tutorials-approuter-identity (IAS) fronts interactive login so native OIDC + # end_session fires on /logout (#2610 — clean logout on shared event machines). + # The former 16.x "one binding only / non-deterministic 500s" constraint no + # longer applies: 23.x supports XSUAA+IAS bound together. Routes set + # authenticationType:ias EXPLICITLY (approuter/xs-app.json) to avoid the + # per-request BTP-security-endpoint probe when both services are bound. + # NOTE: tutorials-identity (the PUBLIC mcp-remote PKCE client, client + # 0b1e8b56-…) is a SEPARATE instance and is intentionally NOT bound here. + # The .well-known MCP issuer stays NON-SECRET config env (XSUAA_MCP_URL + + # XSUAA_MCP_XSAPPNAME, see properties above), NOT a binding. + - name: tutorials-approuter-identity # approuter resolves runtime secrets from the credstore via - # approuter/lib/credstore-secret.js (same binding the srv uses). The retired - # /admin/rebuild handler (#1659 C.4) no longer reads from it. + # approuter/lib/credstore-secret.js (same binding the srv uses). - name: tutorials-credstore - name: srv-api group: destinations @@ -473,6 +477,17 @@ resources: service-name: tutorials-xsuaa config-path: xs-security.json + # IAS instance fronting approuter interactive login so native OIDC end_session + # fires on /logout (#2610 — clean logout on shared event machines, tenant + # atxgsg7zi). Created out-of-band (like tutorials-identity) for a stable + # client_id and adopted here, so no provisioning time is added to the main MTA + # deploy. This is NOT the mcp-remote PUBLIC PKCE client — that is the separate + # tutorials-identity instance, which stays untouched. + - name: tutorials-approuter-identity + type: org.cloudfoundry.existing-service + parameters: + service-name: tutorials-approuter-identity + - name: tutorials-destination type: org.cloudfoundry.managed-service parameters: diff --git a/.gitleaks.toml b/.gitleaks.toml index 4de773bdc..5664506c0 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -43,9 +43,21 @@ description = "Project-specific false positives" # NOTE: the finding is anchored to the commit that introduced line 40; if that # comment is ever reworded, re-add the new introducing commit here (or drop the # literal PEM markers from the doc). Confirmed a placeholder (2026-07-23). +# Suppress approuter/lib/github-oidc-keys.js (line 48, RuleID private-key) in +# commit 75778103a: the `ensurePem()` helper contains the literal PEM armor +# ("-----BEGIN PRIVATE KEY-----\n${wrapped}\n-----END PRIVATE KEY-----") it +# uses to WRAP a credstore-sourced key string into PEM form. The literal is a +# template, not a key body — no real secret (the actual signing key lives only +# in the BTP Credential Store, alias GITHUB_OIDC_SIGNING_KEY). gitleaks' +# private-key rule matches the header/footer literal in the code. Because +# history is scanned (fetch-depth 0), this fails every PR after #2618 merged to +# DEV. Same class as the srv/lib/github-app-token.js entry above. NOTE: anchored +# to the introducing commit; if the helper is reworded, re-add the new commit +# (or drop the literal PEM markers). Confirmed a placeholder (2026-10-03). commits = [ "5153670f57cb30d9322af3603642fdaf980add74", "5a6dedf4345733040e9148dd503b6a62ed5a2b4c", + "75778103a5b7c54be258feba8f268cc2fa41b687", ] # Env-var references are the CORRECT, secret-free pattern — never flag them. regexes = [ diff --git a/CLAUDE.md b/CLAUDE.md index eb3243fe6..c12c46a06 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,6 +101,15 @@ Subsystem one-liners: `mbt build` only `cp`'s `hugo/public/` into the approuter — it does **not** run Hugo or `fetch-tutorials`. Local builds MUST run `npm run build:all` before `mbt build`; skipping ships stale approuter (missing NEW badges, license icons, progress UI). Always confirm deploy scope with maintainer (backend-only / +content / +QA). +- **The MCP server is a SEPARATE MTA (`tutorials-mcp`, `mta-mcp.yaml`)** — the command above deploys only the main `tutorials-ims` MTA (`.deploy/mta.yaml`), which serves the anonymous `/mcp/*`, PAT `/mcp-pat/*`, and admin `/mcp-admin/*` surfaces. The authenticated `/mcp-auth/*` surface runs in the `tutorials-srv-mcp` app of the separate `tutorials-mcp` MTA. `npm run deploy` / `scripts/deploy-mta.cjs` do NOT cover it — deploy it by hand from repo root: + + ```bash + mbt build -e mta-mcp.yaml -t mta_archives + cf deploy mta_archives/*.mtar -e deploy/mcp-dev.mtaext -f # or deploy/mcp-prod.mtaext + ``` + + The MCP MTA adopts the main MTA's services (hana/xsuaa/identity/credstore/destination) as `existing-service`, so deploy the main MTA first on a fresh space. Feature flag `MCP_AUTH_ENABLED` targets `tutorials-srv-mcp`; `MCP_PAT_MINT_ENABLED`/`KG_STEP_SLICER_ENABLED` target `tutorials-srv`. See [mta-deployment.md § MCP MTA](docs/developers/operations/mta-deployment.md#mcp-mta-tutorials-mcp). + - **Local deploy is envsubst-free** — All four secrets (`CONTENT_API_KEY`, `REBUILD_API_KEY`, `APPROUTER_URL`, `GITHUB_DISPATCH_TOKEN`) formerly injected via `envsubst` now live exclusively in the BTP Credential Store (or have been removed entirely, in APPROUTER_URL's case). Run `cf deploy mta_archives/*.mtar -e ../deploy/dev.mtaext -f` directly. Rotation happens through `/admin-ui/#secrets` on the target env's approuter. See [mta-deployment.md](docs/developers/operations/mta-deployment.md) "Local deploy no longer needs envsubst" for the full context. - **PR over direct merge** — Default to `gh pr create` from a feature branch; subagent code review is not a substitute for PR review. Only fast-merge to `main` if explicitly told to skip the PR. diff --git a/README.md b/README.md index f205739e1..c2e1b2199 100644 --- a/README.md +++ b/README.md @@ -113,14 +113,17 @@ tutorials-ims/ │ ├── improvements.md, TODO.md# Feature backlog and gap tracking (largely historic) │ └── superpowers/specs/+plans/ # Feature specs and step-by-step implementation plans ├── .deploy/ # MTA build + deploy artifacts -│ ├── mta.yaml # MTA descriptor (modules: approuter, srv, srv-qa, db, db-qa, destinations) +│ ├── mta.yaml # Main MTA descriptor (modules: approuter, srv, srv-qa, db, db-qa, destinations) │ ├── xs-security.json # XSUAA scopes + role collections (Admin, MobileApp, Tutorial.Author) │ ├── deploy-admin.sh # Standalone admin UI deploy helper (bypasses MTA build) │ └── DEPLOY.md # Deploy procedure documentation +├── mta-mcp.yaml # Separate MCP MTA descriptor (tutorials-mcp → module tutorials-srv-mcp, serves /mcp-auth/*; adopts main-MTA services) ├── deploy/ # MTA extension descriptors (environment overrides) -│ ├── dev.mtaext # Development overrides (instance counts, memory) +│ ├── dev.mtaext # Main-MTA development overrides (instance counts, memory, srv-mcp-url) │ ├── qa.mtaext # QA/staging overrides -│ └── prod.mtaext # Production overrides +│ ├── prod.mtaext # Production overrides +│ ├── mcp-dev.mtaext # MCP-MTA DEV overrides (tutorials-mcp-dev) +│ └── mcp-prod.mtaext # MCP-MTA PROD overrides (tutorials-mcp-prod) ├── .github/workflows/ # CI/CD pipelines (GitHub Actions) │ ├── deploy.yml # Build MTA + deploy to BTP CF + post-deploy smoke tests │ ├── rebuild-content.yml # Re-fetch tutorials + rebuild Hugo + publish HTML to HANA diff --git a/app/admin-annotations.cds b/app/admin-annotations.cds index 2fb8c45c1..5a283a3ce 100644 --- a/app/admin-annotations.cds +++ b/app/admin-annotations.cds @@ -4229,6 +4229,7 @@ annotate AdminService.PetSubmissions with @( UI.LineItem: [ { $Type: 'UI.DataField', Value: petName, Label: 'Pet Name' }, { $Type: 'UI.DataField', Value: uploaderName, Label: 'Uploader' }, + { $Type: 'UI.DataField', Value: uploaderEmail, Label: 'Email' }, { $Type: 'UI.DataField', Value: contestTitle, Label: 'Contest' }, { $Type: 'UI.DataField', Value: moderation, Label: 'Status' }, { $Type: 'UI.DataField', Value: uploadedAt, Label: 'Uploaded' }, @@ -4246,6 +4247,7 @@ annotate AdminService.PetSubmissions with @( UI.FieldGroup#Details: { Data: [ { Value: petName, Label: 'Pet Name' }, { Value: uploaderName, Label: 'Uploader' }, + { Value: uploaderEmail, Label: 'Email' }, { Value: contestTitle, Label: 'Contest' }, { Value: contestSlug, Label: 'Contest Slug' }, { Value: moderation, Label: 'Status' }, @@ -4253,7 +4255,7 @@ annotate AdminService.PetSubmissions with @( { Value: uploadedAt, Label: 'Uploaded At' } ]}, UI.Facets: [ - { $Type: 'UI.ReferenceFacet', Label: 'Details', Target: '@UI.FieldGroup#Details' } + { $Type: 'UI.ReferenceFacet', ID: 'Details', Label: 'Details', Target: '@UI.FieldGroup#Details' } ], UI.Identification: [ { $Type: 'UI.DataFieldForAction', Action: 'AdminService.approve', Label: 'Approve' }, @@ -4265,10 +4267,29 @@ annotate AdminService.PetSubmissions with @( Capabilities.UpdateRestrictions: { Updatable: false } ); +// Quick-filter tabs (#2597): one-click "Pending" vs "All" views with counts. +// Wired as single-table quickVariantSelection in the FE manifest's +// controlConfiguration for the LineItem. SelectionVariant#All uses an empty +// SelectOptions so it shows the full queue. +annotate AdminService.PetSubmissions with @( + UI.SelectionVariant #All: { + Text: 'All', + SelectOptions: [] + }, + UI.SelectionVariant #Pending: { + Text: 'Pending', + SelectOptions: [{ + PropertyName: moderation, + Ranges: [{ Sign: #I, Option: #EQ, Low: 'PENDING' }] + }] + } +); + annotate AdminService.PetSubmissions with { ID @Common.Label: 'ID'; petName @Common.Label: 'Pet Name'; uploaderName @Common.Label: 'Uploader'; + uploaderEmail @Common.Label: 'Email'; moderation @Common.Label: 'Status'; sizeBytes @Common.Label: 'Size (bytes)'; uploadedAt @Common.Label: 'Uploaded At'; diff --git a/app/admin-shell/webapp/model/navigation.json b/app/admin-shell/webapp/model/navigation.json index 2612410f8..5dce31908 100644 --- a/app/admin-shell/webapp/model/navigation.json +++ b/app/admin-shell/webapp/model/navigation.json @@ -154,7 +154,7 @@ "requiredScope": "Admin", "items": [ { "key": "baipAiLaunchpad", "title": "AI Launchpad", "href": "https://tutorial-system.ai-launchpad.prod.eu-central-1.aws.ai-prod.cloud.sap/aic/index.html", "target": "_blank", "requiredScope": "Admin" }, - { "key": "baipBtpSubaccount", "title": "BTP Subaccount", "href": "https://emea.cockpit.btp.cloud.sap/cockpit/#/globalaccount/a0ec0d63-f690-42ab-8113-9f6567cd897e/subaccount/3c6fa3f1-db8c-4e47-9048-fa8c84b867cb/subaccountoverview", "target": "_blank", "requiredScope": "Admin" }, + { "key": "baipBtpSubaccount", "title": "SAP Business AI Platform Subaccount", "href": "https://emea.cockpit.btp.cloud.sap/cockpit/#/globalaccount/a0ec0d63-f690-42ab-8113-9f6567cd897e/subaccount/3c6fa3f1-db8c-4e47-9048-fa8c84b867cb/subaccountoverview", "target": "_blank", "requiredScope": "Admin" }, { "key": "baipHanaCloud", "title": "HANA Cloud", "href": "https://devrel.hana-tooling.ingress.orchestration.prod-eu10.hanacloud.ondemand.com/hcs/sap/hana/cloud/index.html", "target": "_blank", "requiredScope": "Admin" }, { "key": "baipIasAdminConsole", "title": "IAS Admin Console", "href": "https://atxgsg7zi.accounts.ondemand.com/admin/#/applications/17b636dd-fa87-4cd8-b0f2-0abd5f9a3042", "target": "_blank", "requiredScope": "Admin" } ] diff --git a/app/admin/joule/webapp/i18n/i18n.properties b/app/admin/joule/webapp/i18n/i18n.properties index ad2610c07..9cc21f8f7 100644 --- a/app/admin/joule/webapp/i18n/i18n.properties +++ b/app/admin/joule/webapp/i18n/i18n.properties @@ -72,6 +72,6 @@ a2aTokenUrlPlaceholder=XSUAA token endpoint advertised in the Agent Card alertsSection=Operational Alerting (SAP Alert Notification) alertsEnabled=Enable ANS push alerts -alertsEnabledHelp=Master switch for SAP Alert Notification push alerts (publish-reject, scheduled-job failures, rebuild-dispatch failures). Off (default) = no alerts sent. Requires the ANS email action to be configured in the BTP cockpit first. DB-backed — takes effect live, no redeploy needed. +alertsEnabledHelp=Master switch for SAP Alert Notification push alerts (publish-reject, scheduled-job failures, rebuild-dispatch failures). Off (default) = no alerts sent. Requires the ANS email action to be configured in the SAP Business AI Platform cockpit first. DB-backed — takes effect live, no redeploy needed. sendTestAlertButton=Send test alert -sendTestAlertHelp=Fires a TEST alert (eventType AlertingTest) through the real ANS code path so you can confirm end-to-end delivery on demand. Enable ANS push alerts above and click Save FIRST — the toggle only takes effect after saving (a ~5s server cache). A delivered result means the code path fired; the email also needs an AlertingTest condition wired in the BTP cockpit for the target environment. +sendTestAlertHelp=Fires a TEST alert (eventType AlertingTest) through the real ANS code path so you can confirm end-to-end delivery on demand. Enable ANS push alerts above and click Save FIRST — the toggle only takes effect after saving (a ~5s server cache). A delivered result means the code path fired; the email also needs an AlertingTest condition wired in the SAP Business AI Platform cockpit for the target environment. diff --git a/app/admin/joule/webapp/view/Settings.view.xml b/app/admin/joule/webapp/view/Settings.view.xml index 39b617e1e..944ab7f88 100644 --- a/app/admin/joule/webapp/view/Settings.view.xml +++ b/app/admin/joule/webapp/view/Settings.view.xml @@ -191,7 +191,7 @@ scheduled-job failures, and rebuild-dispatch failures (srv/lib/alerting.js, resolved via srv/lib/runtime-config/alert-settings.js). DB-backed (no env var) so ops can toggle live without a restart. The ANS - email action must be configured in the BTP cockpit first, else alerts + email action must be configured in the SAP Business AI Platform cockpit first, else alerts have nowhere to go. Rollback by flipping this back off — no redeploy. --> diff --git a/app/admin/petoberfest/webapp/ext/PhotoThumb.fragment.xml b/app/admin/petoberfest/webapp/ext/PhotoThumb.fragment.xml index 6d78237e8..58d0988dc 100644 --- a/app/admin/petoberfest/webapp/ext/PhotoThumb.fragment.xml +++ b/app/admin/petoberfest/webapp/ext/PhotoThumb.fragment.xml @@ -1,11 +1,14 @@ + diff --git a/app/admin/petoberfest/webapp/ext/PhotoView.fragment.xml b/app/admin/petoberfest/webapp/ext/PhotoView.fragment.xml new file mode 100644 index 000000000..d2a1e6228 --- /dev/null +++ b/app/admin/petoberfest/webapp/ext/PhotoView.fragment.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/admin/petoberfest/webapp/manifest.json b/app/admin/petoberfest/webapp/manifest.json index 1cf121cba..18de0a9f3 100644 --- a/app/admin/petoberfest/webapp/manifest.json +++ b/app/admin/petoberfest/webapp/manifest.json @@ -87,6 +87,19 @@ "tableSettings": { "selectionMode": "ForceMulti" }, + "quickVariantSelection": { + "showCounts": true, + "paths": [ + { + "key": "All", + "annotationPath": "com.sap.vocabularies.UI.v1.SelectionVariant#All" + }, + { + "key": "Pending", + "annotationPath": "com.sap.vocabularies.UI.v1.SelectionVariant#Pending" + } + ] + }, "columns": { "PhotoThumbColumn": { "header": "Photo", @@ -112,7 +125,21 @@ "options": { "settings": { "contextPath": "/PetSubmissions", - "editableHeaderContent": false + "editableHeaderContent": false, + "content": { + "body": { + "sections": { + "PhotoSection": { + "title": "Photo", + "position": { + "placement": "Before", + "anchor": "Details" + }, + "template": "sap.tutorials.admin.petoberfest.ext.PhotoView" + } + } + } + } } } }, diff --git a/app/admin/puzzles/webapp/view/Builder.view.xml b/app/admin/puzzles/webapp/view/Builder.view.xml index 4b10b48c5..01355a355 100644 --- a/app/admin/puzzles/webapp/view/Builder.view.xml +++ b/app/admin/puzzles/webapp/view/Builder.view.xml @@ -53,7 +53,7 @@