fix(schedules): schedules and watches run as the data-analysis service principal #19
No files matched your search
@@ -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
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<qqq.snapshot.version>4.1.0-RC.15</qqq.snapshot.version>
|
||||
<qqq.snapshot.version>4.1.0-RC.19</qqq.snapshot.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
@@ -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<String, String> recipientAddressSupplier;
|
||||
|
||||
@@ -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<QSession> 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()));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user