diff --git a/src/main/java/com/kingsrook/qbits/crm/health/CrmHealthChecks.java b/src/main/java/com/kingsrook/qbits/crm/health/CrmHealthChecks.java index 7ff3d04..16bf3b6 100644 --- a/src/main/java/com/kingsrook/qbits/crm/health/CrmHealthChecks.java +++ b/src/main/java/com/kingsrook/qbits/crm/health/CrmHealthChecks.java @@ -43,6 +43,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; import com.kingsrook.qqq.middleware.health.HealthIndicator; import com.kingsrook.qqq.middleware.health.model.metadata.HealthCheckResult; import com.kingsrook.qqq.middleware.health.model.metadata.HealthStatus; @@ -51,7 +52,8 @@ import com.kingsrook.qqq.middleware.health.model.metadata.HealthStatus; /******************************************************************************* ** STAND-IN(core=QRun-IO/qqq#1199, removeBy=4.1.0-RC.9) ** CRM checks for hosts to register with their health middleware. Run in the - ** caller's tenant context; no check elevates access or returns personal values. + ** caller's tenant context when one is supplied, else as the core system user; + ** no check returns personal values. *******************************************************************************/ public class CrmHealthChecks { @@ -86,17 +88,21 @@ public class CrmHealthChecks /******************************************************************************* - ** Expose individual checks through the RC.6 health SPI. + ** Expose individual checks through the RC.6 health SPI. With no caller + ** session (registration at startup), each check runs as the core system + ** user: token-based authentication modules (OAuth2, Auth0) reject any other + ** tokenless session, and the checks report only UP/DEGRADED, never values. *******************************************************************************/ public List indicators() { - return (indicators(QContext.getQSession() == null ? new QSession() : QContext.getQSession())); + return (indicators(QContext.getQSession())); } /******************************************************************************* ** Supply the host's monitoring session explicitly for middleware worker threads. ** Its permissions and tenant keys are preserved; this adapter never grants keys. + ** A null session runs each check as the core system user. *******************************************************************************/ public List indicators(QSession session) { @@ -136,7 +142,7 @@ public class CrmHealthChecks CapturedContext original = QContext.capture(); try { - QContext.init(instance, session); + QContext.init(instance, session != null ? session : new QSystemUserSession(instance)); return (switch(name) { case "crm.email" -> email(); diff --git a/src/test/java/com/kingsrook/qbits/crm/CrmHealthBulkLoadTest.java b/src/test/java/com/kingsrook/qbits/crm/CrmHealthBulkLoadTest.java index 408125a..91819fb 100644 --- a/src/test/java/com/kingsrook/qbits/crm/CrmHealthBulkLoadTest.java +++ b/src/test/java/com/kingsrook/qbits/crm/CrmHealthBulkLoadTest.java @@ -35,6 +35,9 @@ import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.savedbulkloadprofiles.SavedBulkLoadProfileMetaDataProvider; import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.model.BulkLoadProfile; import com.kingsrook.qqq.middleware.health.model.metadata.HealthStatus; @@ -186,4 +189,35 @@ class CrmHealthBulkLoadTest extends BaseTest assertThat(QContext.getQInstance()).isSameAs(instance); } + + + /******************************************************************************* + ** Regression: indicators registered with no caller session (at host startup) + ** must still run on a host whose authentication rejects tokenless sessions + ** (OAuth2, Auth0). They used a plain new QSession(), so every check failed + ** session validation and reported UNKNOWN. + *******************************************************************************/ + @Test + void contextlessIndicatorsRunOnTokenRequiredHost() throws Exception + { + var instance = QContext.getQInstance(); + instance.withInstanceDefaultAuthentication(new QAuthenticationMetaData() + .withName("token-required") + .withType(QAuthenticationType.MOCK) + .withModuleCodeReference(new QCodeReference(TokenRequiredAuthenticationModule.class))); + var status = java.util.concurrent.CompletableFuture.supplyAsync(() -> + { + try + { + var indicator = new CrmHealthChecks(new CrmQBitConfig()).indicators().get(1); + return (indicator.check(instance).getStatus()); + } + catch(Exception e) + { + throw (new AssertionError(e)); + } + }).get(); + assertThat(status).isEqualTo(HealthStatus.UP); + } + } diff --git a/src/test/java/com/kingsrook/qbits/crm/TokenRequiredAuthenticationModule.java b/src/test/java/com/kingsrook/qbits/crm/TokenRequiredAuthenticationModule.java new file mode 100644 index 0000000..5d4bdaa --- /dev/null +++ b/src/test/java/com/kingsrook/qbits/crm/TokenRequiredAuthenticationModule.java @@ -0,0 +1,72 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. 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.crm; + + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; + + +/******************************************************************************* + ** Test authentication module that behaves like the token-based core modules + ** (OAuth2, Auth0): a QSystemUserSession is always valid, a session it created + ** is valid, and any other session has no token and fails validation with the + ** same message the OAuth2 module produces (JWTDecodeException: The token is + ** null). + *******************************************************************************/ +public class TokenRequiredAuthenticationModule implements QAuthenticationModuleInterface +{ + private static final Set ISSUED = ConcurrentHashMap.newKeySet(); + + + + /******************************************************************************* + ** Issue a session that carries a token. + *******************************************************************************/ + @Override + public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException + { + QSession session = new QSession(); + ISSUED.add(session.getUuid()); + return (session); + } + + + + /******************************************************************************* + ** Accept the system user and issued sessions; reject tokenless sessions. + *******************************************************************************/ + @Override + public boolean isSessionValid(QInstance instance, QSession session) + { + if(session instanceof QSystemUserSession || ISSUED.contains(session.getUuid())) + { + return (true); + } + throw (new IllegalStateException("The token is null")); + } +}