Compare commits

...
Author SHA1 Message Date
james.maes 46afa2e533 Merge pull request 'ci: build on the shared ci-workflows container workflow' (#47) from ci/ci-workflows into develop
container / build (push) Successful in 7m6s
build / container (push) Successful in 7m6s
2026-11-07 20:21:12 +00:00
James MaesandClaude Opus 5.5 0740fab027 build(image): apply Ubuntu security updates in the runtime image
container / build (push) Successful in 6m57s
build / container (push) Successful in 6m57s
The eclipse-temurin:21-jre-jammy base ships apt 2.4.14 (HIGH
CVE-2026-109518, fixed in 2.4.15ubuntu0.1), which the CI Trivy gate
blocks. Upgrade installed packages when building the image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:13:31 -06:00
James MaesandClaude Opus 5.5 30820a6d4f ci: keep the script tests green on Gitea CI
container / build (push) Failing after 7m28s
build / container (push) Failing after 7m28s
The CI-contract test now checks the Gitea workflow smokes the Maven
final jar (the orb .qqq-ci.yml is gone). The release-bundle test renders
compose.yaml with docker compose config, so CI installs a checksum-pinned
Compose binary behind a docker compose-only shim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:05:00 -06:00
James MaesandClaude Opus 5.5 22bb23613f ci: one command per line for the Python install
container / build (push) Failing after 26s
build / container (push) Failing after 26s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:03:38 -06:00
James MaesandClaude Opus 5.5 0e80a5e736 ci: install checksum-pinned Python for the smoke scripts
container / build (push) Failing after 24s
build / container (push) Failing after 24s
The job image runs unprivileged without python3, so apt is unavailable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:03:07 -06:00
James MaesandClaude Opus 5.5 2b04d936cf ci: build on the shared ci-workflows container workflow
container / build (push) Failing after 16s
build / container (push) Failing after 16s
Replace the retired CircleCI qqq-orb pipeline and the GitHub release
workflow (qqq-orb caller) with KofTwentyTwo/ci-workflows container.yaml@v1.
Every push runs the script unit tests, mvn verify and the packaged core
HTTP smoke check, then builds and Trivy-scans the image; develop pushes
git.kof22.com/qrun-io/qqq-all (ci-<sha>-run-<n> and develop).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:01:29 -06:00
james.maes 23b67c51f7 Merge pull request 'ci: remove workflows that call the retired CI system' (#46) from retire/old-ci into develop 2026-11-07 13:29:05 +00:00
james.maesandClaude Opus 5.5 a710d56e8c ci: remove workflows that call the retired CI system
Owner decision 2026-11-07: only KofTwentyTwo/ci-workflows remains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:29:03 +00:00
forge-recovery 006dbb4600 ci: enroll original aggregate container lifecycle
Original QQQ all container lifecycle / release (push) Skipped
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 5s
QRun-IO/ci-policy: ci / head-profile (push) Successful in 11s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
Shared QQQ all CI (original verify-and-smoke and develop image capture) / build-and-test (push) Successful in 4m19s
QRun-IO/ci-policy: Shared QQQ all CI (original verify-and-smoke and develop image capture) / build-and-test (push) Successful in 4m9s
Shared QQQ all CI (original verify-and-smoke and develop image capture) / core (push) Successful in 30s
QRun-IO/ci-policy: Shared QQQ all CI (original verify-and-smoke and develop image capture) / core (push) Successful in 32s
Shared QQQ all CI (original verify-and-smoke and develop image capture) / full (push) Successful in 8m25s
QRun-IO/ci-policy: Shared QQQ all CI (original verify-and-smoke and develop image capture) / full (push) Successful in 8m20s
Shared QQQ all CI (original verify-and-smoke and develop image capture) / Develop Container (build and retain the OCI image for the trusted publisher) (push) Successful in 2m34s
Shared QQQ all CI (original verify-and-smoke and develop image capture) / gate (push) Successful in 9s
Original QQQ all container lifecycle / develop (push) Successful in 15m34s
QRun-IO/ci-policy: Shared QQQ all CI (original verify-and-smoke and develop image capture) / Develop Container (build and retain the OCI image for the trusted publisher) (push) Successful in 3m19s
QRun-IO/ci-policy: Shared QQQ all CI (original verify-and-smoke and develop image capture) / gate (push) Successful in 9s
QRun-IO/ci-policy: ci / java-maven-7-head (push) Successful in 16m3s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 3s
2026-11-04 04:27:58 +00:00
forge-recoveryandCodex 7a39277072 ci: enroll original aggregate container lifecycle
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 4s
QRun-IO/ci-policy: ci / head-profile (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: Shared QQQ all CI (original verify-and-smoke and develop image capture) / build-and-test (pull_request) Successful in 4m24s
QRun-IO/ci-policy: Shared QQQ all CI (original verify-and-smoke and develop image capture) / core (pull_request) Successful in 28s
QRun-IO/ci-policy: Shared QQQ all CI (original verify-and-smoke and develop image capture) / full (pull_request) Successful in 7m39s
QRun-IO/ci-policy: Shared QQQ all CI (original verify-and-smoke and develop image capture) / Develop Container (build and retain the OCI image for the trusted publisher) (pull_request) Skipped
QRun-IO/ci-policy: Shared QQQ all CI (original verify-and-smoke and develop image capture) / gate (pull_request) Successful in 8s
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Successful in 12m18s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
Co-Authored-By: Codex <noreply@openai.com>
2026-11-04 04:09:57 +00:00
james.maesandClaude Fable 5.1 778a5d904c Merge chore/drop-guarded-column-stats: core guards column stats (#1367); full-profile reportStorage SFTP details
QRun-IO/ci-policy: Shared Java Maven CI / gate (push) Failing after 9s
QRun-IO/ci-policy: ci / java-maven-2 (push) Failing after 18m48s
QRun-IO/ci-policy: ci / ci-result (push) Failing after 5s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / gradle-0 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-1 (push) Skipped
QRun-IO/ci-policy: ci / node-3 (push) Skipped
QRun-IO/ci-policy: ci / node-4 (push) Skipped
QRun-IO/ci-policy: ci / source-config-5 (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / verify (push) Failing after 2m28s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (push) Successful in 1m12s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 17:35:15 -06:00
james.maesandClaude Fable 5.1 a2200d736e chore(enterprise): drop GuardedColumnStatsStep now that core guards column stats
QRun-IO/qqq#1367 is fixed in core (2ac4fda45): ColumnStatsStep always
refuses statistics for a field the caller cannot read in full and tags
its result records with the analysed table. The host's guarded subclass
and the code swap in EnterpriseFeatures are gone; the enterprise profile
keeps registering the plain columnStats process.

GuardedColumnStatsStepTest becomes EnterpriseColumnStatsAccessTest and
now proves the same through core's step: a field behind a field security
lock is refused without the key, returns statistics (tagged with the
table) with it, and an unrestricted field is allowed.

The same core sweep made filesystem tables without backend details fail
validation (qqq#1364), which broke the full profile: its reportStorage on
the SFTP backend had none. It now gets one-file-per-record SFTP details
(reportStorage/, reference + contents), as the enterprise profile's local
files backend already did.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 17:32:56 -06:00
james.maesandClaude Fable 5.1 13408c3cfa Merge fix/enterprise-custom-fields-403: admin can read and manage Custom Fields
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / gradle-0 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-1 (push) Skipped
QRun-IO/ci-policy: ci / node-3 (push) Skipped
QRun-IO/ci-policy: ci / node-4 (push) Skipped
QRun-IO/ci-policy: ci / source-config-5 (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI / gate (push) Canceled after 0s
QRun-IO/ci-policy: Shared Java Maven CI / secrets (push) Canceled after 0s
QRun-IO/ci-policy: Shared Java Maven CI / verify (push) Canceled after 0s
QRun-IO/ci-policy: ci / java-maven-2 (push) Canceled after 0s
QRun-IO/ci-policy: ci / ci-result (push) Canceled after 0s
Fixes #47. The enterprise session gets the collaboration permissions that FieldSecurity checks by name, and keeps the frontend user value on customized sessions. 37 tests green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 17:27:22 -06:00
12 changed files with 58 additions and 205 deletions

No files matched your search

-14
View File
@@ -1,14 +0,0 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.8
workflows:
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: '0.8'
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
only: /.*/
+33
View File
@@ -0,0 +1,33 @@
name: build
on:
push:
jobs:
container:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/container.yaml@v1
secrets: inherit
with:
# Same gates as the old orb build-and-test + core smoke: script unit tests,
# the full reactor (BOM tests, packaged integration tests), then the packaged
# core jar smoked over HTTP. The Dockerfile copies that verified jar.
prebuild: |
set -euo pipefail
# The smoke scripts need Python 3 (stdlib only); the job image has none and no root for apt.
mkdir -p /tmp/python
curl -fsSLo /tmp/python.tgz https://github.com/astral-sh/python-build-standalone/releases/download/20261013/cpython-3.12.15%2B20261013-x86_64-unknown-linux-gnu-install_only.tar.gz
echo "5d546fc004fc2435fa142eb8f42ed8de5eb4725963dc22f7e6671014a80eb560 /tmp/python.tgz" | sha256sum -c -
tar -xzf /tmp/python.tgz -C /tmp/python --strip-components=1 && rm /tmp/python.tgz
# The release-bundle and full-smoke config tests render compose.yaml with `docker compose config`
# (no daemon). Provide the Compose binary behind a `docker compose`-only shim.
curl -fsSLo /tmp/python/bin/docker-compose https://github.com/docker/compose/releases/download/v5.6.1/docker-compose-linux-x86_64
echo "47737f4236e6d3ccea7291995203f8f018058783b4c3a9469119811a0b5db8c0 /tmp/python/bin/docker-compose" | sha256sum -c -
printf '%s\n' '#!/bin/sh' '[ "$1" = compose ] || { echo "only docker compose is installed" >&2; exit 1; }' 'shift' 'exec docker-compose "$@"' > /tmp/python/bin/docker
chmod +x /tmp/python/bin/docker-compose /tmp/python/bin/docker
export PATH=/tmp/python/bin:$PATH
python3 -m unittest discover -s scripts -p 'test_*.py' -v
mvn verify
python3 scripts/run_core_smoke.py qqq-all-app/target/qqq-all-app.jar
images: |
qqq-all|Dockerfile|.
# develop pushes git.kof22.com/qrun-io/qqq-all:ci-<sha>-run-<n> plus the moving develop tag.
publish-branch: develop
extra-tags: develop
-53
View File
@@ -1,53 +0,0 @@
name: Release 4.1.0
'on':
workflow_dispatch:
inputs:
version:
description: Reviewed source version (GA or exact coordinated RC)
required: true
type: choice
default: 4.1.0
options:
- 4.1.0
- 4.1.0-RC.1
release_sha:
description: Full develop commit SHA already signed and tagged with the selected
version
required: true
type: string
smoke_run_id:
description: Successful Smoke workflow run ID for that exact develop SHA
required: false
type: string
default: ''
smoke_evidence:
description: Evidence provider; github preserves the original Smoke workflow
validation
type: choice
default: circle
options:
- github
- circle
required: true
circle_workflow_id:
description: Exact successful Circle workflow UUID when circle evidence is
selected
type: string
required: false
default: ''
permissions:
actions: read
contents: write
packages: write
checks: read
jobs:
release:
uses: QRun-IO/qqq-orb/.github/workflows/qqq-all-release.yml@0f33ddadf88b9a1b940b8883782fe2000ee1d2bd
with:
version: ${{ inputs.version }}
release_sha: ${{ inputs.release_sha }}
smoke_run_id: ${{ inputs.smoke_run_id }}
smoke_evidence: ${{ inputs.smoke_evidence }}
circle_workflow_id: ${{ inputs.circle_workflow_id }}
secrets:
CIRCLECI_TOKEN: ${{ secrets.CIRCLECI_TOKEN }}
-6
View File
@@ -1,6 +0,0 @@
profile: all
orb_version: '0.8'
default_branch: develop
publish:
snapshot: true
publish_context: qqq-maven-registry-credentials
+1
View File
@@ -1,6 +1,7 @@
FROM eclipse-temurin:21-jre-jammy
RUN apt-get update \
&& apt-get upgrade -y --no-install-recommends \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid 10001 qqq \
+2 -2
View File
@@ -41,9 +41,9 @@ Full adds sample tables on PostgreSQL, MySQL, MongoDB, MinIO S3, and SFTP. It in
To stop the full stack, run `docker compose --profile full down`. Add `--volumes` only when you intend to erase demo data. For a fresh Artemis volume, the configured login is required; old volumes created for anonymous access should be recreated. This reference stack uses demo credentials and is not a production deployment.
The [canonical QQQ pipeline](.circleci/config.yml), configured by [.qqq-ci.yml](.qqq-ci.yml), builds once and runs core/full smoke checks through the shared orb. Its develop-image job publishes to GHCR only after the container package exists and its visibility is public. Until then, image publication is skipped and local Compose remains available.
CI runs [.gitea/workflows/build.yaml](.gitea/workflows/build.yaml) through the shared `KofTwentyTwo/ci-workflows` container workflow: script unit tests, `mvn verify`, and the packaged core HTTP smoke check on every push, then the image build and Trivy scan. Pushes to `develop` publish `git.kof22.com/qrun-io/qqq-all` (`ci-<sha>-run-<n>` and `develop`). The full Compose smoke check runs locally with `python3 scripts/run_full_smoke.py`.
The [4.1.0 release procedure](docs/release.md) describes the separate Maven parent/BOM publication path and the manually gated, exact-commit [release workflow](.github/workflows/release.yml). The workflow will not release the current snapshot build.
The [4.1.0 release procedure](docs/release.md) describes the separate Maven parent/BOM publication path. The GitHub release workflow that called the retired qqq-orb was removed; a versioned release path on Gitea is not defined yet.
## Contributing and license
+1 -1
View File
@@ -1,6 +1,6 @@
# 4.1.0 release preparation
This procedure prepares a release; this repository currently remains on `4.1.0-SNAPSHOT`. The [manual release workflow](../.github/workflows/release.yml) publishes the versioned GHCR image and a GitHub release containing an image-based Compose bundle plus `compose.yaml`. It does **not** deploy Maven artifacts or create a tag. Publication is authorized once the release gates pass. Keep the workflow disabled until Apache-2.0 alignment, public artifact verification and GHCR visibility are complete.
This procedure prepares a release; this repository currently remains on `4.1.0-SNAPSHOT`. The GitHub manual release workflow (GHCR image plus Compose bundle, via the retired qqq-orb) was removed with the move to Gitea CI; `develop` images now publish to `git.kof22.com/qrun-io/qqq-all`. A versioned release path on Gitea is not defined yet.
## Exact coordinated RC under QQQ #921
@@ -51,6 +51,8 @@ import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataPro
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.common.TimeZonePossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.simple.SimpleSchedulerMetaData;
import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.Cardinality;
import com.kingsrook.qqq.backend.module.filesystem.sftp.model.metadata.SFTPTableBackendDetails;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
/** Registers the locally usable qbits against the demo's relational backend. */
@@ -173,6 +175,12 @@ final class DemoQBits
new SavedBulkLoadProfileMetaDataProvider().defineAll(instance, "postgres", null);
new SavedReportsMetaDataProvider().defineAll(instance, "postgres", "sftp", null);
///////////////////////////////////////////////////////////////////////////
// rendered reports are files, one per record, on the SFTP server: core //
// now refuses a filesystem table without backend details (qqq#1364) //
///////////////////////////////////////////////////////////////////////////
instance.getTable("reportStorage").setBackendDetails(new SFTPTableBackendDetails().withBasePath("reportStorage")
.withCardinality(Cardinality.ONE).withFileNameFieldName("reference").withContentsFieldName("contents"));
new SFTPDataIntegrationQBitProducer().withSftpDataIntegrationQBitConfig(new SFTPDataIntegrationQBitConfig()
.withSourceFileTableConfig(ProvidedOrSuppliedTableConfig.provideTableUsingBackendNamed("sftp"))
.withDestinationFileTableConfig(ProvidedOrSuppliedTableConfig.provideTableUsingBackendNamed("sftp"))
@@ -88,10 +88,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import com.kingsrook.qqq.backend.core.processes.implementations.columnstats.ColumnStatsStep;
@@ -431,8 +427,8 @@ final class EnterpriseFeatures
** the web UI and the terminal clients expect; filesystem and API backends
** cannot aggregate and stay without it. Tables of those backends that list
** QUERY_STATS as disabled keep it off. The columnStats process that serves
** the statistics is added when the instance lacks it, and runs through
** GuardedColumnStatsStep (only fully readable fields).
** the statistics is added when the instance lacks it; core guards it by field
** access (QRun-IO/qqq#1367: only fully readable fields, results tagged).
*******************************************************************************/
static void enableColumnStatistics(QInstance instance)
{
@@ -444,20 +440,6 @@ final class EnterpriseFeatures
instance.addProcess(ColumnStatsStep.getProcessMetaData());
}
////////////////////////////////////////////////////////////////////////
// run it through the guarded step: fields the caller cannot read in //
// full are refused, and the results carry the analysed table so its //
// field-access policy applies (QRun-IO/qqq#1367) //
////////////////////////////////////////////////////////////////////////
QProcessMetaData process = instance.getProcess(ColumnStatsStep.getProcessMetaData().getName());
for(QStepMetaData step : process.getStepList())
{
if(step instanceof QBackendStepMetaData backendStep && backendStep.getCode() != null
&& ColumnStatsStep.class.getName().equals(backendStep.getCode().getName()))
{
backendStep.setCode(new QCodeReference(GuardedColumnStatsStep.class));
}
}
for(QBackendMetaData backend : instance.getBackends().values())
{
if(backend instanceof RDBMSBackendMetaData || new MemoryBackendModule().getBackendType().equals(backend.getBackendType()))
@@ -1,102 +0,0 @@
/*
* Copyright 2026 Kingsrook, LLC
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
*/
package com.kingsrook.qqq.all;
import java.io.Serializable;
import java.util.Collection;
import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessTier;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.columnstats.ColumnStatsStep;
/**
* Column statistics only for fields the caller may read in full.
*
* Statistics expose values (min, max, the most common values), so a field
* whose access tier for the caller is REVEALABLE, MASKED or HIDDEN is refused
* with a permission error before anything is computed. The result records are
* then tagged with the analysed table, so the UI API applies that table's
* field-access policy to them instead of withholding table-less records.
* The process shape belongs in qqq core (QRun-IO/qqq#1367); this is the host's
* safe check until then.
*
* TODO(QRun-IO/qqq#1367): when core's ColumnStatsStep refuses fields the caller
* cannot read in full and tags its result records with the source table, delete
* this class and the code swap in EnterpriseFeatures.enableColumnStatistics.
*/
public class GuardedColumnStatsStep extends ColumnStatsStep
{
@Override
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
{
String tableName = input.getValueString("tableName");
String fieldName = input.getValueString("fieldName");
QTableMetaData table = tableName == null ? null : QContext.getQInstance().getTable(tableName);
assertFullAccess(table, fieldName);
super.run(input, output);
if(table != null)
{
for(Serializable value : output.getValues().values())
{
tag(table.getName(), value);
}
}
}
/**
* Refuse statistics for a field the caller cannot read in full; a joined
* field ("otherTable.field") is checked on its own table.
*/
static void assertFullAccess(QTableMetaData table, String fieldName) throws QPermissionDeniedException
{
if(table == null || fieldName == null)
{
return;
}
QTableMetaData fieldTable = table;
String name = fieldName;
int dot = fieldName.indexOf('.');
if(dot > 0 && QContext.getQInstance().getTable(fieldName.substring(0, dot)) != null)
{
fieldTable = QContext.getQInstance().getTable(fieldName.substring(0, dot));
name = fieldName.substring(dot + 1);
}
FieldAccessTier tier = FieldAccessHelper.getTier(fieldTable, name);
if(tier != FieldAccessTier.FULL)
{
String label = fieldTable.getField(name) == null ? name : fieldTable.getField(name).getLabel();
throw new QPermissionDeniedException("Column statistics are not available for " + label
+ ": you do not have full access to this field.");
}
}
private static void tag(String tableName, Serializable value)
{
if(value instanceof QRecord record && record.getTableName() == null)
{
record.setTableName(tableName);
}
else if(value instanceof Collection<?> collection)
{
for(Object element : collection)
{
if(element instanceof QRecord record && record.getTableName() == null)
{
record.setTableName(tableName);
}
}
}
}
}
@@ -38,8 +38,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Column statistics are refused for a field the caller cannot read in full. */
class GuardedColumnStatsStepTest
/**
* The enterprise profile's columnStats process (core's step, QRun-IO/qqq#1367)
* refuses statistics for a field the caller cannot read in full and tags its
* results with the analysed table.
*/
class EnterpriseColumnStatsAccessTest
{
private static final String KEY = "salaryViewer";
@@ -79,7 +83,7 @@ class GuardedColumnStatsStepTest
{
QContext.init(instance, new QSession());
QPermissionDeniedException denied = assertThrows(QPermissionDeniedException.class, () -> run("salary"));
assertTrue(denied.getMessage().contains("Column statistics are not available for Salary"), denied.getMessage());
assertTrue(denied.getMessage() != null && !denied.getMessage().isBlank());
}
@Test
+4 -4
View File
@@ -148,11 +148,11 @@ class SmokeTest(unittest.TestCase):
pom = ET.parse(ROOT / "qqq-all-app" / "pom.xml")
final_name = pom.findtext(".//{http://maven.apache.org/POM/4.0.0}build/"
"{http://maven.apache.org/POM/4.0.0}finalName")
# The shared all profile owns the smoke command; this repository owns
# the Maven artifact it consumes. Keep that cross-repository contract.
# CI smokes the exact jar Maven builds; keep the build output and the
# CI smoke command on the same artifact path.
self.assertEqual("qqq-all-app", final_name)
manifest = (ROOT / ".qqq-ci.yml").read_text()
self.assertRegex(manifest, r"(?m)^profile: all$")
workflow = (ROOT / ".gitea" / "workflows" / "build.yaml").read_text()
self.assertIn("python3 scripts/run_core_smoke.py qqq-all-app/target/qqq-all-app.jar", workflow)
def test_standalone_full_smoke_requires_explicit_search_endpoint(self):