From 2c1fa83cfd7a2a7b19e51002e7f7f1d14ad93a44 Mon Sep 17 00:00:00 2001 From: James Maes Date: Thu, 15 Oct 2026 16:19:18 -0500 Subject: [PATCH 1/3] ci(release-rc): pass secrets to the shared workflow so it can sign the RC tag QRun-IO/.github#2 adds a tag-and-release job that signs vX with the QRUN_TAG_SIGNING_KEY secret after Maven Central confirms the artifact. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release-rc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release-rc.yml b/.github/workflows/release-rc.yml index dd332a0..a342ba8 100644 --- a/.github/workflows/release-rc.yml +++ b/.github/workflows/release-rc.yml @@ -21,5 +21,6 @@ jobs: release-rc: if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc')) uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main + secrets: inherit with: dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} -- 2.54.0 From 21671b3df3cebbb10310c2ca337c9d48199f04fb Mon Sep 17 00:00:00 2001 From: James Maes Date: Fri, 16 Oct 2026 08:27:23 -0500 Subject: [PATCH 2/3] fix(schedules): run schedules and watches as the data-analysis service principal The self-made data-analysis-scheduler session is rejected under OAuth2, so every scheduled run failed. With the host's principal declared, inventory runs as it and owner/recipient runs go on their behalf through it (current access intersected). Undeclared: unchanged. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../dataanalysis/DataAnalysisQBitConfig.java | 22 +++++ .../schedule/AnalysisSessions.java | 90 +++++++++++++++++++ .../dataanalysis/schedule/ScheduleRunner.java | 21 ++--- .../dataanalysis/watch/WatchEvaluator.java | 21 ++--- .../qbits/dataanalysis/SchedulesTest.java | 24 +++++ 6 files changed, 159 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/kingsrook/qbits/dataanalysis/schedule/AnalysisSessions.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 50992f6..a7c5114 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic ### Fixed +- Scheduled analyses and watches work under OAuth2. When the host declares the `data-analysis` service principal (QQQ service identities, `DataAnalysisQBitConfig.withServicePrincipal`), schedule and watch inventory runs as that principal, and owner and recipient runs go on their behalf through it: their current access intersected with the principal's. The self-made `data-analysis-scheduler` session was rejected under OAuth2 (`Invalid session in request` on every `data_analysis_run_schedules` run). With no principal declared, behavior is unchanged. Needs the QQQ release with service identities (qqq#1255, #1256). - QBit metadata now reports the Maven artifact version, including release versions, from a filtered build resource. ### Added diff --git a/src/main/java/com/kingsrook/qbits/dataanalysis/DataAnalysisQBitConfig.java b/src/main/java/com/kingsrook/qbits/dataanalysis/DataAnalysisQBitConfig.java index 64858c6..922c88e 100644 --- a/src/main/java/com/kingsrook/qbits/dataanalysis/DataAnalysisQBitConfig.java +++ b/src/main/java/com/kingsrook/qbits/dataanalysis/DataAnalysisQBitConfig.java @@ -427,6 +427,28 @@ public class DataAnalysisQBitConfig implements QBitConfig return (this); } + private String servicePrincipal = "data-analysis"; + + /******************************************************************************* + ** The service principal schedules and watches run as when the host declares + ** it (QQQ service identities): inventory as the principal, owner and + ** recipient runs on their behalf through it. Undeclared: the previous local + ** sessions. + *******************************************************************************/ + public String getServicePrincipal() + { + return (servicePrincipal); + } + + /******************************************************************************* + ** Fluent setter for servicePrincipal (default data-analysis). + *******************************************************************************/ + public DataAnalysisQBitConfig withServicePrincipal(String servicePrincipal) + { + this.servicePrincipal = servicePrincipal; + return (this); + } + private String messagingProviderName; private String fromAddress; private transient java.util.function.Function recipientAddressSupplier; diff --git a/src/main/java/com/kingsrook/qbits/dataanalysis/schedule/AnalysisSessions.java b/src/main/java/com/kingsrook/qbits/dataanalysis/schedule/AnalysisSessions.java new file mode 100644 index 0000000..4814926 --- /dev/null +++ b/src/main/java/com/kingsrook/qbits/dataanalysis/schedule/AnalysisSessions.java @@ -0,0 +1,90 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2026. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kingsrook.qbits.dataanalysis.schedule; + + +import java.util.function.Supplier; +import com.kingsrook.qbits.dataanalysis.DataAnalysisQBitConfig; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; +import com.kingsrook.qqq.backend.core.serviceidentity.ServiceIdentities; + + +/******************************************************************************* + ** Sessions for scheduled analysis work. With the host's data-analysis service + ** principal declared (QQQ service identities): inventory runs as the + ** principal, and owner or recipient runs go on their behalf through it (their + ** current access intersected with the principal's). Without it: the previous + ** local sessions (single-node default unchanged). Never a system session. + *******************************************************************************/ +public final class AnalysisSessions +{ + /******************************************************************************* + ** Utility class. + *******************************************************************************/ + private AnalysisSessions() + { + } + + + + /******************************************************************************* + ** The session for schedule/watch inventory and status updates. + *******************************************************************************/ + public static QSession maintenance(DataAnalysisQBitConfig config, Supplier legacy) throws QException + { + if(usesPrincipal(config)) + { + return (ServiceIdentities.session(QContext.getQInstance(), config.getServicePrincipal())); + } + return (legacy.get()); + } + + + + /******************************************************************************* + ** The session for running as a user (owner or recipient). + *******************************************************************************/ + public static QSession forUser(DataAnalysisQBitConfig config, String userId) throws QException + { + if(config.getOwnerSessionSupplier() != null) + { + return (config.getOwnerSessionSupplier().apply(userId)); + } + if(usesPrincipal(config)) + { + return (ServiceIdentities.sessionOnBehalfOf(QContext.getQInstance(), config.getServicePrincipal(), userId)); + } + return (new QAuthenticationModuleDispatcher().getQModule(QContext.getQInstance().getAuthentication()).createAutomatedSessionForUser(QContext.getQInstance(), userId)); + } + + + + /******************************************************************************* + ** Whether the host declared the configured principal. + *******************************************************************************/ + private static boolean usesPrincipal(DataAnalysisQBitConfig config) + { + return (config.getServicePrincipal() != null && ServiceIdentities.isDeclared(QContext.getQInstance(), config.getServicePrincipal())); + } +} diff --git a/src/main/java/com/kingsrook/qbits/dataanalysis/schedule/ScheduleRunner.java b/src/main/java/com/kingsrook/qbits/dataanalysis/schedule/ScheduleRunner.java index 5aee7b8..4d63023 100644 --- a/src/main/java/com/kingsrook/qbits/dataanalysis/schedule/ScheduleRunner.java +++ b/src/main/java/com/kingsrook/qbits/dataanalysis/schedule/ScheduleRunner.java @@ -58,7 +58,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QUser; -import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; /******************************************************************************* ** Scheduled work uses owner and recipient sessions; maintenance only inventories rows. @@ -83,13 +82,17 @@ public class ScheduleRunner *******************************************************************************/ public synchronized Integer runDue(Instant now) throws QException { - QSession maintenance = new QSession().withUser(new QUser().withIdReference("data-analysis-scheduler")) - .withPermissions("data_analysis.saved.read", "data_analysis.saved.edit") - .withSecurityKeyValue(DataAnalysisPermissions.ADMIN, true); - if(QContext.getQSession().getBackendVariants() != null) + QSession maintenance = AnalysisSessions.maintenance(config, () -> { - maintenance.setBackendVariants(new java.util.HashMap<>(QContext.getQSession().getBackendVariants())); - } + QSession legacy = new QSession().withUser(new QUser().withIdReference("data-analysis-scheduler")) + .withPermissions("data_analysis.saved.read", "data_analysis.saved.edit") + .withSecurityKeyValue(DataAnalysisPermissions.ADMIN, true); + if(QContext.getQSession().getBackendVariants() != null) + { + legacy.setBackendVariants(new java.util.HashMap<>(QContext.getQSession().getBackendVariants())); + } + return (legacy); + }); int[] count = {0}; QContext.withTemporaryContext(new CapturedContext(QContext.getQInstance(), maintenance), () -> { @@ -161,9 +164,7 @@ public class ScheduleRunner QSession session; try { - session = config.getOwnerSessionSupplier() == null - ? new QAuthenticationModuleDispatcher().getQModule(QContext.getQInstance().getAuthentication()).createAutomatedSessionForUser(QContext.getQInstance(), id) - : config.getOwnerSessionSupplier().apply(id); + session = AnalysisSessions.forUser(config, id); } catch(Exception e) { diff --git a/src/main/java/com/kingsrook/qbits/dataanalysis/watch/WatchEvaluator.java b/src/main/java/com/kingsrook/qbits/dataanalysis/watch/WatchEvaluator.java index 5a814c7..7f785eb 100644 --- a/src/main/java/com/kingsrook/qbits/dataanalysis/watch/WatchEvaluator.java +++ b/src/main/java/com/kingsrook/qbits/dataanalysis/watch/WatchEvaluator.java @@ -49,7 +49,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QUser; -import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; /******************************************************************************* ** Transition watches through the core alerts SPI, computing exclusively as owner. @@ -94,13 +93,17 @@ public class WatchEvaluator *******************************************************************************/ public synchronized Integer evaluateEnabled(Instant now) throws QException { - QSession maintenance = new QSession().withUser(new QUser().withIdReference("data-analysis-scheduler")) - .withPermissions("data_analysis.watch.read", "data_analysis.watch.edit") - .withSecurityKeyValue(DataAnalysisPermissions.ADMIN, true); - if(QContext.getQSession().getBackendVariants() != null) + QSession maintenance = com.kingsrook.qbits.dataanalysis.schedule.AnalysisSessions.maintenance(config, () -> { - maintenance.setBackendVariants(new java.util.HashMap<>(QContext.getQSession().getBackendVariants())); - } + QSession legacy = new QSession().withUser(new QUser().withIdReference("data-analysis-scheduler")) + .withPermissions("data_analysis.watch.read", "data_analysis.watch.edit") + .withSecurityKeyValue(DataAnalysisPermissions.ADMIN, true); + if(QContext.getQSession().getBackendVariants() != null) + { + legacy.setBackendVariants(new java.util.HashMap<>(QContext.getQSession().getBackendVariants())); + } + return (legacy); + }); int[] count = {0}; QContext.withTemporaryContext(new CapturedContext(QContext.getQInstance(), maintenance), () -> { @@ -128,9 +131,7 @@ public class WatchEvaluator QSession session; try { - session = config.getOwnerSessionSupplier() == null - ? new QAuthenticationModuleDispatcher().getQModule(QContext.getQInstance().getAuthentication()).createAutomatedSessionForUser(QContext.getQInstance(), owner) - : config.getOwnerSessionSupplier().apply(owner); + session = com.kingsrook.qbits.dataanalysis.schedule.AnalysisSessions.forUser(config, owner); } catch(Exception e) { diff --git a/src/test/java/com/kingsrook/qbits/dataanalysis/SchedulesTest.java b/src/test/java/com/kingsrook/qbits/dataanalysis/SchedulesTest.java index 0bd49da..04fdaae 100644 --- a/src/test/java/com/kingsrook/qbits/dataanalysis/SchedulesTest.java +++ b/src/test/java/com/kingsrook/qbits/dataanalysis/SchedulesTest.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import com.kingsrook.qbits.dataanalysis.model.DataAnalysisTables; +import com.kingsrook.qbits.dataanalysis.permissions.DataAnalysisPermissions; import com.kingsrook.qbits.dataanalysis.processes.ScheduleActions; import com.kingsrook.qbits.dataanalysis.saved.AnalysisExport; import com.kingsrook.qbits.dataanalysis.saved.SavedDefinition; @@ -47,6 +48,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.messaging.QMessagingProvide import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.messaging.MessagingProviderInterface; import com.kingsrook.qqq.backend.core.modules.messaging.QMessagingProviderDispatcher; +import com.kingsrook.qqq.backend.core.serviceidentity.QServicePrincipalMetaData; +import com.kingsrook.qqq.backend.core.serviceidentity.ServiceIdentitiesMetaData; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -160,6 +163,27 @@ class SchedulesTest extends EngineFixture assertThat(WatchesTest.Sink.raised).isEmpty(); } + /******************************************************************************* + ** With the host's data-analysis principal declared, schedule inventory runs + ** as the principal (its grants decide - a principal without them finds + ** nothing due), never as a self-made session. + *******************************************************************************/ + @Test + void inventoryRunsAsTheDeclaredPrincipal() throws Exception + { + schedule("PIVOT", Map.of("rows", List.of(Map.of("field", "status")), "values", List.of(Map.of("operator", "COUNT"))), "CSV"); + config.withServicePrincipal("analysis-bare"); + ServiceIdentitiesMetaData.declare(instance, new QServicePrincipalMetaData().withName("analysis-bare")); + assertThatThrownBy(() -> runner.runDue(NOW)).isInstanceOf(QException.class); + assertThat(Mail.sent).isEmpty(); + + ServiceIdentitiesMetaData.declare(instance, new QServicePrincipalMetaData().withName("data-analysis") + .withLocalPermissions("data_analysis.saved.read", "data_analysis.saved.edit").withLocalSecurityKeyValue(DataAnalysisPermissions.ADMIN, true)); + config.withServicePrincipal("data-analysis"); + assertThat(runner.runDue(NOW)).isEqualTo(1); + assertThat(Mail.sent).hasSize(2); + } + @Test void providerFailureAndMissingOwnerRaiseVisibleAlerts() throws Exception { -- 2.54.0 From 20545be7fce8d4a87c68c57aed657aeaa6e04d24 Mon Sep 17 00:00:00 2001 From: James Maes Date: Fri, 16 Oct 2026 13:19:12 -0500 Subject: [PATCH 3/3] build: QQQ 4.1.0-RC.19 Co-Authored-By: Claude Opus 5.5 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e3ff7b7..6ba151f 100644 --- a/pom.xml +++ b/pom.xml @@ -243,7 +243,7 @@ true - 4.1.0-RC.15 + 4.1.0-RC.19 -- 2.54.0