Preserve current rendering, operator and retained-source behavior while integrating script quality, meaningful headers and strict Error Prone/NullAway contracts. Production and test compilation and the 189-case verification gate pass with zero failures and one intentional workload skip. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: James Maes <james@kof22.com>
270 lines
14 KiB
Java
270 lines
14 KiB
Java
/*
|
|
* QQQ - Low-code Application Framework for Engineers.
|
|
* Copyright (C) 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.
|
|
*/
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package com.kingsrook.qbits.printing;
|
|
|
|
|
|
import java.net.URI;
|
|
import java.sql.Connection;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
|
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
|
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
|
import com.kingsrook.qqq.backend.core.model.metadata.authentication.AuthScope;
|
|
import com.kingsrook.qqq.backend.core.model.metadata.authentication.OAuth2AuthenticationMetaData;
|
|
import com.kingsrook.qqq.backend.core.model.metadata.authentication.OAuth2NativeSessionLoginMetaData;
|
|
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
|
|
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
|
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
|
|
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata.RedirectStateMetaDataProducer;
|
|
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata.UserSessionMetaDataProducer;
|
|
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata.UserSessionRefreshTokenMetaDataProducer;
|
|
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.model.UserSession;
|
|
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.model.UserSessionRefreshToken;
|
|
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
|
|
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
|
|
import com.kingsrook.qqq.middleware.javalin.QApplicationJavalinServer;
|
|
import com.kingsrook.qqq.middleware.javalin.QApplicationLauncher;
|
|
import com.kingsrook.qqq.middleware.javalin.QApplicationLauncherConfig;
|
|
import io.javalin.http.Context;
|
|
import io.javalin.http.ForbiddenResponse;
|
|
import org.json.JSONArray;
|
|
import org.json.JSONObject;
|
|
|
|
|
|
/***************************************************************************
|
|
** Opt-in container fixture for the pinned upstream native-session candidate.
|
|
** Standard OAuth2 code exchange, session storage and encrypted renewal are used
|
|
** unchanged. No installed pairing, password authentication double or printer.
|
|
***************************************************************************/
|
|
public final class NativeAuthHost extends AbstractQQQApplication
|
|
{
|
|
private static final String TENANT = "printing-native-dev";
|
|
private final String backendName = "printing-native-auth-" + UUID.randomUUID();
|
|
private final String jdbcUrl = "jdbc:h2:file:/data/printing-native-auth";
|
|
private final String databasePassword = required("NATIVE_QQQ_DATABASE_PASSWORD");
|
|
private final URI origin = httpsOrigin(required("NATIVE_QQQ_ORIGIN"));
|
|
private final int port = Integer.parseInt(required("NATIVE_QQQ_PORT"));
|
|
private final DriverDataSource source = new DriverDataSource(jdbcUrl, "sa", databasePassword);
|
|
private final PrintingService service = new PrintingService(source, TENANT);
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
** Initialize only the private fixture database and native session tables.
|
|
***************************************************************************/
|
|
private NativeAuthHost() throws Exception
|
|
{
|
|
if(port < 1024 || port > 65535)
|
|
{
|
|
throw new IllegalArgumentException("Unsupported internal fixture port");
|
|
}
|
|
service.initialize();
|
|
try(Connection connection = source.getConnection(); var statement = connection.createStatement())
|
|
{
|
|
statement.execute("CREATE TABLE IF NOT EXISTS userSession (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, uuid VARCHAR(40) NOT NULL UNIQUE, userId VARCHAR(100), accessToken CLOB, createDate TIMESTAMP WITH TIME ZONE, modifyDate TIMESTAMP WITH TIME ZONE)");
|
|
statement.execute("CREATE TABLE IF NOT EXISTS redirectState (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, state VARCHAR(45) NOT NULL UNIQUE, redirectUri VARCHAR(4096), createDate TIMESTAMP WITH TIME ZONE)");
|
|
statement.execute("CREATE TABLE IF NOT EXISTS userSessionRefreshToken (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, sessionUuid VARCHAR(40) NOT NULL UNIQUE, refreshToken CLOB, refreshTokenExpiresAt TIMESTAMP WITH TIME ZONE, sessionStartedAt TIMESTAMP WITH TIME ZONE, createDate TIMESTAMP WITH TIME ZONE, modifyDate TIMESTAMP WITH TIME ZONE)");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
** Launch the experimental provider host inside its owned container network.
|
|
***************************************************************************/
|
|
public static void main(String[] args) throws Exception
|
|
{
|
|
NativeAuthHost host = new NativeAuthHost();
|
|
QApplicationLauncher.run(host, new QApplicationLauncherConfig()
|
|
.withServerCustomizer(host::configure));
|
|
}
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
** Define the private fixture backend, native OAuth2 provider and printing QBit.
|
|
***************************************************************************/
|
|
@Override
|
|
public QInstance defineQInstance() throws QException
|
|
{
|
|
QInstance instance = new QInstance();
|
|
instance.setFailOnMetaDataProducerError(true);
|
|
instance.setHomeAppName("printing");
|
|
instance.setBranding(new QBrandingMetaData().withAppName("QQQ native authentication fixture"));
|
|
instance.addBackend(new RDBMSBackendMetaData().withName(backendName).withVendor("h2").withHostName("fixture")
|
|
.withDatabaseName("printing-native-auth").withUsername("sa").withPassword(databasePassword).withJdbcUrl(jdbcUrl));
|
|
instance.addTable(new UserSessionMetaDataProducer(backendName).produce(instance));
|
|
instance.addTable(new RedirectStateMetaDataProducer(backendName).produce(instance));
|
|
instance.addTable(new UserSessionRefreshTokenMetaDataProducer(backendName).produce(instance));
|
|
|
|
String issuer = required("NATIVE_AUTH_ISSUER");
|
|
URI issuerUri = URI.create(issuer);
|
|
if(!"https".equals(issuerUri.getScheme()) || issuerUri.getHost() == null
|
|
|| issuerUri.getRawUserInfo() != null || issuerUri.getRawQuery() != null || issuerUri.getRawFragment() != null || !issuer.endsWith("/"))
|
|
{
|
|
throw new IllegalArgumentException("An explicit HTTPS fixture issuer ending in slash is required");
|
|
}
|
|
OAuth2AuthenticationMetaData authentication = new OAuth2AuthenticationMetaData()
|
|
.withBaseUrl(issuer).withExternalBaseUrl(issuer)
|
|
.withClientId(required("NATIVE_AUTH_CLIENT_ID")).withClientSecret(required("NATIVE_AUTH_CLIENT_SECRET"))
|
|
.withScopes("openid profile email offline_access")
|
|
.withUserSessionTableName(UserSession.TABLE_NAME).withRedirectStateTableName(RedirectStateMetaDataProducer.TABLE_NAME)
|
|
.withRefreshTokenTableName(UserSessionRefreshToken.TABLE_NAME)
|
|
.withRefreshTokensEnabled(true).withRefreshTokenSkewSeconds(0).withMaxSessionLifetimeSeconds(3600)
|
|
.withRefreshTokenEncryptionKey(required("NATIVE_QQQ_REFRESH_KEY")).withRefreshTokenEncryptionKeyId("fixture-1")
|
|
.withNativeSessionLogin(new OAuth2NativeSessionLoginMetaData().withIssuer(issuer)
|
|
.withDiscoveryUrl(issuer + ".well-known/openid-configuration")
|
|
.withRedirectUris(List.of(required("NATIVE_AUTH_REDIRECT_URI"))));
|
|
authentication.withName("printing-native-provider").withSessionStoreEnabled(false)
|
|
.withCustomizer(new QCodeReference(PrintingPermissions.class));
|
|
instance.registerAuthenticationProvider(AuthScope.instanceDefault(), authentication);
|
|
new PrintingQBitProducer().withConfig(new PrintingQBitConfig().withBackendName(backendName).withService(service))
|
|
.produce(instance, "printing");
|
|
for(QTableMetaData table : instance.getTables().values())
|
|
{
|
|
if(backendName.equals(table.getBackendName()))
|
|
{
|
|
table.withBackendDetails(new RDBMSTableBackendDetails().withTableName(table.getName().toUpperCase(Locale.ROOT)));
|
|
for(var field : table.getFields().values())
|
|
{
|
|
field.withBackendName(field.getName().toUpperCase(Locale.ROOT));
|
|
}
|
|
}
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
** Register printing routes behind the fixture TLS proxy.
|
|
***************************************************************************/
|
|
private void configure(QApplicationJavalinServer server)
|
|
{
|
|
// Only the owned TLS proxy is published; this HTTP listener remains inside
|
|
// the Compose network. Forwarded HTTPS is required even for discovery.
|
|
server.withPort(port).withServeFrontendNext(false).withServeFrontendMaterialDashboard(false)
|
|
.withAdditionalRouteProviders(List.of(new PrintingRealtimeRoutes(service)))
|
|
.withJavalinConfigCustomizer(config ->
|
|
{
|
|
config.jetty.host = "0.0.0.0";
|
|
config.http.maxRequestSize = 1048576;
|
|
config.routes.before(this::guardOrigin);
|
|
});
|
|
}
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
** Require the configured HTTPS proxy and same-origin cookie mutations.
|
|
***************************************************************************/
|
|
private void guardOrigin(Context context)
|
|
{
|
|
if(!origin.getRawAuthority().equalsIgnoreCase(context.header("Host"))
|
|
|| !"https".equals(context.header("X-Forwarded-Proto")))
|
|
{
|
|
throw new ForbiddenResponse("The fixture requires its configured HTTPS proxy origin");
|
|
}
|
|
String suppliedOrigin = context.header("Origin");
|
|
if(suppliedOrigin != null && !origin.toString().equals(suppliedOrigin))
|
|
{
|
|
throw new ForbiddenResponse("Cross-origin fixture requests are not allowed");
|
|
}
|
|
if("cross-site".equalsIgnoreCase(context.header("Sec-Fetch-Site"))
|
|
|| context.header("X-QQQ-In-Process-Session") != null)
|
|
{
|
|
throw new ForbiddenResponse("Only standard native session authentication is allowed");
|
|
}
|
|
boolean unsafe = !Set.of("GET", "HEAD", "OPTIONS").contains(context.method().name());
|
|
boolean cookie = context.cookie("sessionUUID") != null || context.cookie("sessionId") != null;
|
|
if(unsafe && cookie && suppliedOrigin == null)
|
|
{
|
|
throw new ForbiddenResponse("Cookie mutations require the configured Origin");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
** Only maps claims after the unchanged standard OAuth2 module authenticates.
|
|
***************************************************************************/
|
|
public static final class PrintingPermissions implements QAuthenticationModuleCustomizerInterface
|
|
{
|
|
/***************************************************************************
|
|
** Grant the printing admin permission only for the fixture group claim.
|
|
***************************************************************************/
|
|
@Override
|
|
public void customizeSession(QInstance instance, QSession session, Map<String, Object> context)
|
|
{
|
|
session.setPermissions(Set.of());
|
|
session.withSecurityKeyValue("printing_tenant", TENANT);
|
|
Object claims = context.get("jwtPayloadJsonObject");
|
|
if(session.getUser() != null && claims instanceof JSONObject payload)
|
|
{
|
|
JSONArray groups = payload.optJSONArray("groups");
|
|
if(groups != null && groups.toList().contains("printing_admin"))
|
|
{
|
|
session.setPermissions(Set.of("printing_admin"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
** Validate the exact root HTTPS origin used by the private TLS fixture.
|
|
***************************************************************************/
|
|
private static URI httpsOrigin(String value)
|
|
{
|
|
URI parsed = URI.create(value);
|
|
if(!"https".equals(parsed.getScheme()) || parsed.getHost() == null || parsed.getRawUserInfo() != null
|
|
|| parsed.getRawQuery() != null || parsed.getRawFragment() != null || !parsed.getRawPath().isEmpty())
|
|
{
|
|
throw new IllegalArgumentException("A root HTTPS fixture origin without trailing slash is required");
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
|
|
|
|
/***************************************************************************
|
|
** Read a required synthetic fixture setting without logging its value.
|
|
***************************************************************************/
|
|
private static String required(String name)
|
|
{
|
|
String value = System.getenv(name);
|
|
if(value == null || value.isBlank())
|
|
{
|
|
throw new IllegalArgumentException("Set the synthetic fixture variable " + name);
|
|
}
|
|
return value;
|
|
}
|
|
}
|