Working version of authentication for static & dynamic (process) route providers

This commit is contained in:
2025-03-12 20:17:16 -05:00
parent 45a6c3bcad
commit 955cb67a2c
11 changed files with 924 additions and 102 deletions

View File

@ -77,8 +77,8 @@ public class QApplicationJavalinServer
private boolean serveLegacyUnversionedMiddlewareAPI = true;
private List<AbstractMiddlewareVersion> middlewareVersionList = List.of(new MiddlewareVersionV1());
private List<QJavalinRouteProviderInterface> additionalRouteProviders = null;
private Consumer<Javalin> javalinConfigurationCustomizer = null;
private QJavalinMetaData javalinMetaData = null;
private Consumer<Javalin> javalinConfigurationCustomizer = null;
private QJavalinMetaData javalinMetaData = null;
private long lastQInstanceHotSwapMillis;
private long millisBetweenHotSwaps = 2500;
@ -197,6 +197,15 @@ public class QApplicationJavalinServer
}
});
//////////////////////////////////////////////////////////////////////
// also pass the javalin service into any additionalRouteProviders, //
// in case they need additional setup, e.g., before/after handlers. //
//////////////////////////////////////////////////////////////////////
for(QJavalinRouteProviderInterface routeProvider : CollectionUtils.nonNullList(additionalRouteProviders))
{
routeProvider.acceptJavalinService(service);
}
//////////////////////////////////////////////////////////////////////////////////////
// per system property, set the server to hot-swap the q instance before all routes //
//////////////////////////////////////////////////////////////////////////////////////
@ -228,7 +237,7 @@ public class QApplicationJavalinServer
/***************************************************************************
** initial tests with the SimpleFileSystemDirectoryRouter would sometimes
** have a Content-Type:text/html;charset=null !
** which doesn't seem every valid (and at least it broke our unit test).
** which doesn't seem ever valid (and at least it broke our unit test).
** so, if w see charset=null in contentType, replace it with the system
** default, which may not be 100% right, but has to be better than "null"...
***************************************************************************/
@ -242,7 +251,6 @@ public class QApplicationJavalinServer
contentType = contentType.replace("charset=null", "charset=" + Charset.defaultCharset().name());
context.res().setContentType(contentType);
}
System.out.println();
});
}
@ -630,6 +638,7 @@ public class QApplicationJavalinServer
}
/*******************************************************************************
** Getter for javalinMetaData
*******************************************************************************/
@ -659,5 +668,4 @@ public class QApplicationJavalinServer
return (this);
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.middleware.javalin;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import io.javalin.Javalin;
import io.javalin.apibuilder.EndpointGroup;
import io.javalin.config.JavalinConfig;
@ -53,7 +54,9 @@ public interface QJavalinRouteProviderInterface
/***************************************************************************
**
** when the javalin service is being configured as part of its boot up,
** accept the javalinConfig object, to perform whatever setup you need,
** such as setting up routes.
***************************************************************************/
default void acceptJavalinConfig(JavalinConfig config)
{
@ -61,4 +64,17 @@ public interface QJavalinRouteProviderInterface
// noop at default //
/////////////////////
}
/***************************************************************************
** when the javalin service is being configured as part of its boot up,
** accept the Javalin service object, to perform whatever setup you need,
** such as setting up before/after handlers.
***************************************************************************/
default void acceptJavalinService(Javalin service)
{
/////////////////////
// noop at default //
/////////////////////
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.middleware.javalin.metadata;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QMetaDataObject;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
@ -38,6 +39,8 @@ public class JavalinRouteProviderMetaData implements QMetaDataObject
private List<String> methods;
private QCodeReference routeAuthenticator;
/*******************************************************************************
@ -172,4 +175,35 @@ public class JavalinRouteProviderMetaData implements QMetaDataObject
return (this);
}
/*******************************************************************************
** Getter for routeAuthenticator
*******************************************************************************/
public QCodeReference getRouteAuthenticator()
{
return (this.routeAuthenticator);
}
/*******************************************************************************
** Setter for routeAuthenticator
*******************************************************************************/
public void setRouteAuthenticator(QCodeReference routeAuthenticator)
{
this.routeAuthenticator = routeAuthenticator;
}
/*******************************************************************************
** Fluent setter for routeAuthenticator
*******************************************************************************/
public JavalinRouteProviderMetaData withRouteAuthenticator(QCodeReference routeAuthenticator)
{
this.routeAuthenticator = routeAuthenticator;
return (this);
}
}

View File

@ -27,22 +27,31 @@ import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.tables.StorageAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.storage.StorageInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import com.kingsrook.qqq.backend.javalin.QJavalinUtils;
import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface;
import io.javalin.apibuilder.ApiBuilder;
import io.javalin.apibuilder.EndpointGroup;
import io.javalin.http.Context;
import io.javalin.http.HttpStatus;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -55,7 +64,10 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
private final String hostedPath;
private final String processName;
private final List<String> methods;
private QInstance qInstance;
private QCodeReference routeAuthenticator;
private QInstance qInstance;
@ -76,6 +88,7 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
public ProcessBasedRouter(JavalinRouteProviderMetaData routeProvider)
{
this(routeProvider.getHostedPath(), routeProvider.getProcessName(), routeProvider.getMethods());
setRouteAuthenticator(routeProvider.getRouteAuthenticator());
}
@ -145,41 +158,36 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
RunProcessInput input = new RunProcessInput();
input.setProcessName(processName);
try
QContext.init(qInstance, new QSystemUserSession());
boolean isAuthenticated = false;
if(routeAuthenticator == null)
{
QJavalinImplementation.setupSession(context, input);
isAuthenticated = true;
}
catch(Exception e)
else
{
context.header("WWW-Authenticate", "Basic realm=\"Access to this QQQ site\"");
context.status(HttpStatus.UNAUTHORIZED);
try
{
RouteAuthenticatorInterface routeAuthenticator = QCodeLoader.getAdHoc(RouteAuthenticatorInterface.class, this.routeAuthenticator);
isAuthenticated = routeAuthenticator.authenticateRequest(context);
}
catch(Exception e)
{
context.skipRemainingHandlers();
QJavalinImplementation.handleException(context, e);
}
}
if(!isAuthenticated)
{
LOG.info("Request is not authenticated, so returning before running process", logPair("processName", processName), logPair("path", context.path()));
return;
}
/*
boolean authorized = false;
String authorization = context.header("Authorization");
if(authorization != null && authorization.matches("^Basic .+"))
{
String base64Authorization = authorization.substring("Basic ".length());
String decoded = new String(Base64.getDecoder().decode(base64Authorization), StandardCharsets.UTF_8);
String[] parts = decoded.split(":", 2);
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
}
if(!authorized)
{
}
// todo - not always system-user session!!
QContext.init(this.qInstance, new QSystemUserSession());
*/
try
{
LOG.info("Running [" + processName + "] to serve [" + context.path() + "]...");
LOG.info("Running process to serve route", logPair("processName", processName), logPair("path", context.path()));
/////////////////////
// run the process //
@ -190,16 +198,10 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
input.addValue("pathParams", new HashMap<>(context.pathParamMap()));
input.addValue("queryParams", new HashMap<>(context.queryParamMap()));
input.addValue("formParams", new HashMap<>(context.formParamMap()));
RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
input.addValue("cookies", new HashMap<>(context.cookieMap()));
input.addValue("requestHeaders", new HashMap<>(context.headerMap()));
/////////////////
// status code //
/////////////////
Integer statusCode = runProcessOutput.getValueInteger("statusCode");
if(statusCode != null)
{
context.status(statusCode);
}
RunProcessOutput runProcessOutput = new RunProcessAction().execute(input);
/////////////////
// headers map //
@ -217,26 +219,46 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
// maybe via the callback object??? input.setCallback(new QProcessCallback() {});
// context.resultInputStream();
///////////////////
// response body //
///////////////////
Serializable response = runProcessOutput.getValue("response");
if(response instanceof String s)
//////////////
// response //
//////////////
Integer statusCode = runProcessOutput.getValueInteger("statusCode");
String redirectURL = runProcessOutput.getValueString("redirectURL");
String responseString = runProcessOutput.getValueString("responseString");
byte[] responseBytes = runProcessOutput.getValueByteArray("responseBytes");
StorageInput responseStorageInput = (StorageInput) runProcessOutput.getValue("responseStorageInput");
if(StringUtils.hasContent(redirectURL))
{
context.result(s);
context.redirect(redirectURL, statusCode == null ? HttpStatus.FOUND : HttpStatus.forStatus(statusCode));
return;
}
else if(response instanceof byte[] ba)
if(statusCode != null)
{
context.result(ba);
context.status(statusCode);
}
else if(response instanceof InputStream is)
if(StringUtils.hasContent(responseString))
{
context.result(is);
context.result(responseString);
return;
}
else
if(responseBytes != null && responseBytes.length > 0)
{
context.result(ValueUtils.getValueAsString(response));
context.result(responseBytes);
return;
}
if(responseStorageInput != null)
{
InputStream inputStream = new StorageAction().getInputStream(responseStorageInput);
context.result(inputStream);
return;
}
throw (new QException("No response value was set in the process output state."));
}
catch(Exception e)
{
@ -248,4 +270,35 @@ public class ProcessBasedRouter implements QJavalinRouteProviderInterface
}
}
/*******************************************************************************
** Getter for routeAuthenticator
*******************************************************************************/
public QCodeReference getRouteAuthenticator()
{
return (this.routeAuthenticator);
}
/*******************************************************************************
** Setter for routeAuthenticator
*******************************************************************************/
public void setRouteAuthenticator(QCodeReference routeAuthenticator)
{
this.routeAuthenticator = routeAuthenticator;
}
/*******************************************************************************
** Fluent setter for routeAuthenticator
*******************************************************************************/
public ProcessBasedRouter withRouteAuthenticator(QCodeReference routeAuthenticator)
{
this.routeAuthenticator = routeAuthenticator;
return (this);
}
}

View File

@ -0,0 +1,412 @@
/*
* 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/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.routeproviders;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
import com.kingsrook.qqq.backend.core.model.actions.processes.QProcessPayload;
/*******************************************************************************
** process payload shared the processes which are used as process-based-router
** processes. e.g., the fields here are those written to and read by
** ProcessBasedRouter.
*******************************************************************************/
public class ProcessBasedRouterPayload extends QProcessPayload
{
private String path;
private String method;
private Map<String, String> pathParams;
private Map<String, String> queryParams;
private Map<String, String> formParams;
private Map<String, String> cookies;
private Integer statusCode;
private String redirectURL;
private Map<String, String> responseHeaders;
private String responseString;
private byte[] responseBytes;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ProcessBasedRouterPayload()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ProcessBasedRouterPayload(ProcessState processState)
{
this.populateFromProcessState(processState);
}
/*******************************************************************************
** Getter for path
*******************************************************************************/
public String getPath()
{
return (this.path);
}
/*******************************************************************************
** Setter for path
*******************************************************************************/
public void setPath(String path)
{
this.path = path;
}
/*******************************************************************************
** Fluent setter for path
*******************************************************************************/
public ProcessBasedRouterPayload withPath(String path)
{
this.path = path;
return (this);
}
/*******************************************************************************
** Getter for method
*******************************************************************************/
public String getMethod()
{
return (this.method);
}
/*******************************************************************************
** Setter for method
*******************************************************************************/
public void setMethod(String method)
{
this.method = method;
}
/*******************************************************************************
** Fluent setter for method
*******************************************************************************/
public ProcessBasedRouterPayload withMethod(String method)
{
this.method = method;
return (this);
}
/*******************************************************************************
** Getter for pathParams
*******************************************************************************/
public Map<String, String> getPathParams()
{
return (this.pathParams);
}
/*******************************************************************************
** Setter for pathParams
*******************************************************************************/
public void setPathParams(Map<String, String> pathParams)
{
this.pathParams = pathParams;
}
/*******************************************************************************
** Fluent setter for pathParams
*******************************************************************************/
public ProcessBasedRouterPayload withPathParams(Map<String, String> pathParams)
{
this.pathParams = pathParams;
return (this);
}
/*******************************************************************************
** Getter for queryParams
*******************************************************************************/
public Map<String, String> getQueryParams()
{
return (this.queryParams);
}
/*******************************************************************************
** Setter for queryParams
*******************************************************************************/
public void setQueryParams(Map<String, String> queryParams)
{
this.queryParams = queryParams;
}
/*******************************************************************************
** Fluent setter for queryParams
*******************************************************************************/
public ProcessBasedRouterPayload withQueryParams(Map<String, String> queryParams)
{
this.queryParams = queryParams;
return (this);
}
/*******************************************************************************
** Getter for formParams
*******************************************************************************/
public Map<String, String> getFormParams()
{
return (this.formParams);
}
/*******************************************************************************
** Setter for formParams
*******************************************************************************/
public void setFormParams(Map<String, String> formParams)
{
this.formParams = formParams;
}
/*******************************************************************************
** Fluent setter for formParams
*******************************************************************************/
public ProcessBasedRouterPayload withFormParams(Map<String, String> formParams)
{
this.formParams = formParams;
return (this);
}
/*******************************************************************************
** Getter for cookies
*******************************************************************************/
public Map<String, String> getCookies()
{
return (this.cookies);
}
/*******************************************************************************
** Setter for cookies
*******************************************************************************/
public void setCookies(Map<String, String> cookies)
{
this.cookies = cookies;
}
/*******************************************************************************
** Fluent setter for cookies
*******************************************************************************/
public ProcessBasedRouterPayload withCookies(Map<String, String> cookies)
{
this.cookies = cookies;
return (this);
}
/*******************************************************************************
** Getter for statusCode
*******************************************************************************/
public Integer getStatusCode()
{
return (this.statusCode);
}
/*******************************************************************************
** Setter for statusCode
*******************************************************************************/
public void setStatusCode(Integer statusCode)
{
this.statusCode = statusCode;
}
/*******************************************************************************
** Fluent setter for statusCode
*******************************************************************************/
public ProcessBasedRouterPayload withStatusCode(Integer statusCode)
{
this.statusCode = statusCode;
return (this);
}
/*******************************************************************************
** Getter for responseHeaders
*******************************************************************************/
public Map<String, String> getResponseHeaders()
{
return (this.responseHeaders);
}
/*******************************************************************************
** Setter for responseHeaders
*******************************************************************************/
public void setResponseHeaders(Map<String, String> responseHeaders)
{
this.responseHeaders = responseHeaders;
}
/*******************************************************************************
** Fluent setter for responseHeaders
*******************************************************************************/
public ProcessBasedRouterPayload withResponseHeaders(Map<String, String> responseHeaders)
{
this.responseHeaders = responseHeaders;
return (this);
}
/*******************************************************************************
** Getter for responseString
*******************************************************************************/
public String getResponseString()
{
return (this.responseString);
}
/*******************************************************************************
** Setter for responseString
*******************************************************************************/
public void setResponseString(String responseString)
{
this.responseString = responseString;
}
/*******************************************************************************
** Fluent setter for responseString
*******************************************************************************/
public ProcessBasedRouterPayload withResponseString(String responseString)
{
this.responseString = responseString;
return (this);
}
/*******************************************************************************
** Getter for responseBytes
*******************************************************************************/
public byte[] getResponseBytes()
{
return (this.responseBytes);
}
/*******************************************************************************
** Setter for responseBytes
*******************************************************************************/
public void setResponseBytes(byte[] responseBytes)
{
this.responseBytes = responseBytes;
}
/*******************************************************************************
** Fluent setter for responseBytes
*******************************************************************************/
public ProcessBasedRouterPayload withResponseBytes(byte[] responseBytes)
{
this.responseBytes = responseBytes;
return (this);
}
/*******************************************************************************
** Getter for redirectURL
*******************************************************************************/
public String getRedirectURL()
{
return (this.redirectURL);
}
/*******************************************************************************
** Setter for redirectURL
*******************************************************************************/
public void setRedirectURL(String redirectURL)
{
this.redirectURL = redirectURL;
}
/*******************************************************************************
** Fluent setter for redirectURL
*******************************************************************************/
public ProcessBasedRouterPayload withRedirectURL(String redirectURL)
{
this.redirectURL = redirectURL;
return (this);
}
}

View File

@ -22,22 +22,40 @@
package com.kingsrook.qqq.middleware.javalin.routeproviders;
import java.net.URL;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.RouteAuthenticatorInterface;
import io.javalin.Javalin;
import io.javalin.config.JavalinConfig;
import io.javalin.http.Context;
import io.javalin.http.staticfiles.Location;
import io.javalin.http.staticfiles.StaticFileConfig;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
** javalin route provider that hosts a path in the http server via a path on
** the file system
*******************************************************************************/
public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInterface
{
private final String hostedPath;
private final String fileSystemPath;
private QInstance qInstance;
private static final QLogger LOG = QLogger.getLogger(SimpleFileSystemDirectoryRouter.class);
private final String hostedPath;
private final String fileSystemPath;
private QCodeReference routeAuthenticator;
private QInstance qInstance;
@ -59,6 +77,7 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
public SimpleFileSystemDirectoryRouter(JavalinRouteProviderMetaData routeProvider)
{
this(routeProvider.getHostedPath(), routeProvider.getFileSystemPath());
setRouteAuthenticator(routeProvider.getRouteAuthenticator());
}
@ -74,17 +93,132 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
/***************************************************************************
**
***************************************************************************/
private void handleJavalinStaticFileConfig(StaticFileConfig staticFileConfig)
{
URL resource = getClass().getClassLoader().getResource(fileSystemPath);
if(resource == null)
{
String message = "Could not find file system path: " + fileSystemPath;
if(fileSystemPath.startsWith("/") && getClass().getClassLoader().getResource(fileSystemPath.replaceFirst("^/+", "")) != null)
{
message += ". For non-absolute paths, do not prefix with a leading slash.";
}
throw new RuntimeException(message);
}
if(!hostedPath.startsWith("/"))
{
LOG.warn("hostedPath [" + hostedPath + "] should probably start with a leading slash...");
}
staticFileConfig.directory = resource.getFile();
staticFileConfig.hostedPath = hostedPath;
staticFileConfig.location = Location.EXTERNAL;
}
/***************************************************************************
**
***************************************************************************/
private void before(Context context) throws QException
{
LOG.debug("In before handler for simpleFileSystemRouter", logPair("hostedPath", hostedPath));
QContext.init(qInstance, new QSystemUserSession());
if(routeAuthenticator != null)
{
try
{
RouteAuthenticatorInterface routeAuthenticator = QCodeLoader.getAdHoc(RouteAuthenticatorInterface.class, this.routeAuthenticator);
boolean isAuthenticated = routeAuthenticator.authenticateRequest(context);
if(!isAuthenticated)
{
LOG.info("Static file request is not authenticated, so telling javalin to skip remaining handlers", logPair("path", context.path()));
context.skipRemainingHandlers();
}
}
catch(Exception e)
{
context.skipRemainingHandlers();
QJavalinImplementation.handleException(context, e);
}
}
}
/***************************************************************************
**
***************************************************************************/
private void after(Context context)
{
LOG.debug("In after handler for simpleFileSystemRouter", logPair("hostedPath", hostedPath));
QContext.clear();
}
/***************************************************************************
**
***************************************************************************/
@Override
public void acceptJavalinConfig(JavalinConfig config)
{
config.staticFiles.add((StaticFileConfig userConfig) ->
{
userConfig.hostedPath = hostedPath;
userConfig.directory = fileSystemPath;
userConfig.location = Location.EXTERNAL;
});
config.staticFiles.add(this::handleJavalinStaticFileConfig);
}
/***************************************************************************
**
***************************************************************************/
@Override
public void acceptJavalinService(Javalin service)
{
String javalinPath = hostedPath;
if(!javalinPath.endsWith("/"))
{
javalinPath += "/";
}
javalinPath += "<subPath>";
service.before(javalinPath, this::before);
service.before(javalinPath, this::after);
}
/*******************************************************************************
** Getter for routeAuthenticator
*******************************************************************************/
public QCodeReference getRouteAuthenticator()
{
return (this.routeAuthenticator);
}
/*******************************************************************************
** Setter for routeAuthenticator
*******************************************************************************/
public void setRouteAuthenticator(QCodeReference routeAuthenticator)
{
this.routeAuthenticator = routeAuthenticator;
}
/*******************************************************************************
** Fluent setter for routeAuthenticator
*******************************************************************************/
public SimpleFileSystemDirectoryRouter withRouteAuthenticator(QCodeReference routeAuthenticator)
{
this.routeAuthenticator = routeAuthenticator;
return (this);
}
}

View File

@ -0,0 +1,43 @@
/*
* 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/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.routeproviders.authentication;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import io.javalin.http.Context;
/*******************************************************************************
** interface used by QJavalinRouteProviderInterface subclasses, to interact with
** QQQ Authentication modules, to provide authentication to custom javalin routes.
*******************************************************************************/
public interface RouteAuthenticatorInterface
{
/***************************************************************************
** where authentication for a route occurs, before the route is served.
**
** @return true if request is authenticated; else false
***************************************************************************/
boolean authenticateRequest(Context context) throws QException;
}

View File

@ -0,0 +1,76 @@
/*
* 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/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.middleware.javalin.routeproviders.authentication;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import io.javalin.http.Context;
/*******************************************************************************
** simple implementation of a route authenticator. Assumes that unauthenticated
** requests should redirect to a login page. Note though, maybe that should be
** more intelligent, like, only redirect requets for a .html file, but not
** requests for include files like images or .js/.css?
*******************************************************************************/
public class SimpleRouteAuthenticator implements RouteAuthenticatorInterface
{
private static final QLogger LOG = QLogger.getLogger(SimpleRouteAuthenticator.class);
/***************************************************************************
**
***************************************************************************/
public boolean authenticateRequest(Context context) throws QException
{
try
{
QSession qSession = QJavalinImplementation.setupSession(context, null);
LOG.debug("Session has been activated", "uuid=" + qSession.getUuid());
return (true);
}
catch(QAuthenticationException e)
{
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(QContext.getQInstance().getAuthentication());
String redirectURL = authenticationModule.getLoginRedirectUrl(context.fullUrl());
context.redirect(redirectURL);
LOG.debug("Redirecting request, due to required session missing");
return (false);
}
catch(QModuleDispatchException e)
{
throw (new QException("Error authenticating request", e));
}
}
}

View File

@ -22,10 +22,8 @@
package com.kingsrook.qqq.backend.javalin;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.HashMap;
@ -98,6 +96,8 @@ import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
import com.kingsrook.qqq.middleware.javalin.metadata.JavalinRouteProviderMetaData;
import com.kingsrook.qqq.middleware.javalin.routeproviders.ProcessBasedRouterPayload;
import com.kingsrook.qqq.middleware.javalin.routeproviders.authentication.SimpleRouteAuthenticator;
import org.apache.commons.io.IOUtils;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -126,7 +126,7 @@ public class TestUtils
public static final String SCREEN_0 = "screen0";
public static final String SCREEN_1 = "screen1";
public static final String STATIC_SITE_PATH = Paths.get("").toAbsolutePath() + "/static-site";
public static final String STATIC_SITE_PATH = "static-site";
@ -134,12 +134,9 @@ public class TestUtils
** Prime a test database (e.g., h2, in-memory)
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public static void primeTestDatabase() throws Exception
{
ConnectionManager connectionManager = new ConnectionManager();
try(Connection connection = connectionManager.getConnection(TestUtils.defineDefaultH2Backend()))
try(Connection connection = ConnectionManager.getConnection(TestUtils.defineDefaultH2Backend()))
{
InputStream primeTestDatabaseSqlStream = TestUtils.class.getResourceAsStream("/prime-test-database.sql");
assertNotNull(primeTestDatabaseSqlStream);
@ -161,8 +158,7 @@ public class TestUtils
*******************************************************************************/
public static void runTestSql(String sql, QueryManager.ResultSetProcessor resultSetProcessor) throws Exception
{
ConnectionManager connectionManager = new ConnectionManager();
try(Connection connection = connectionManager.getConnection(defineDefaultH2Backend()))
try(Connection connection = ConnectionManager.getConnection(defineDefaultH2Backend()))
{
QueryManager.executeStatement(connection, sql, resultSetProcessor);
}
@ -195,17 +191,24 @@ public class TestUtils
defineWidgets(qInstance);
List<JavalinRouteProviderMetaData> routeProviders = new ArrayList<>();
if(new File(STATIC_SITE_PATH).exists())
{
routeProviders.add(new JavalinRouteProviderMetaData()
.withHostedPath("/statically-served")
.withFileSystemPath(STATIC_SITE_PATH));
}
routeProviders.add(new JavalinRouteProviderMetaData()
.withHostedPath("/statically-served")
.withFileSystemPath(STATIC_SITE_PATH));
routeProviders.add(new JavalinRouteProviderMetaData()
.withHostedPath("/protected-statically-served")
.withFileSystemPath(STATIC_SITE_PATH)
.withRouteAuthenticator(new QCodeReference(SimpleRouteAuthenticator.class)));
routeProviders.add(new JavalinRouteProviderMetaData()
.withHostedPath("/served-by-process/<pagePath>")
.withProcessName("routerProcess"));
routeProviders.add(new JavalinRouteProviderMetaData()
.withHostedPath("/protected-served-by-process/<pagePath>")
.withProcessName("routerProcess")
.withRouteAuthenticator(new QCodeReference(SimpleRouteAuthenticator.class)));
qInstance.withSupplementalMetaData(new QJavalinMetaData().withRouteProviders(routeProviders));
qInstance.addBackend(defineMemoryBackend());
@ -237,8 +240,10 @@ public class TestUtils
.withName("step")
.withCode(new QCodeReferenceLambda<BackendStep>((runBackendStepInput, runBackendStepOutput) ->
{
String path = runBackendStepInput.getValueString("path");
runBackendStepOutput.addValue("response", "So you've asked for: " + path);
ProcessBasedRouterPayload processPayload = runBackendStepInput.getProcessPayload(ProcessBasedRouterPayload.class);
String path = processPayload.getPath();
processPayload.setResponseString("So you've asked for: " + path);
runBackendStepOutput.setProcessPayload(processPayload);
}))
));
}
@ -793,7 +798,7 @@ public class TestUtils
{
return (new RenderWidgetOutput(new RawHTML("title",
QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE_OFFSET_MINUTES)
+ "|" + QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE)
+ "|" + QContext.getQSession().getValue(QSession.VALUE_KEY_USER_TIMEZONE)
)));
}
}

View File

@ -22,23 +22,18 @@
package com.kingsrook.qqq.middleware.javalin;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
import java.util.concurrent.TimeUnit;
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.utils.SleepUtils;
import com.kingsrook.qqq.backend.javalin.TestUtils;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
import io.javalin.http.HttpStatus;
import kong.unirest.HttpResponse;
import kong.unirest.Unirest;
import org.apache.commons.io.FileUtils;
import org.json.JSONObject;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -55,17 +50,6 @@ class QApplicationJavalinServerTest
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void beforeEach() throws IOException
{
FileUtils.writeStringToFile(new File(TestUtils.STATIC_SITE_PATH + "/foo.html"), "Foo? Bar!", Charset.defaultCharset());
}
/*******************************************************************************
**
*******************************************************************************/
@ -74,8 +58,6 @@ class QApplicationJavalinServerTest
{
javalinServer.stop();
TestApplication.callCount = 0;
FileUtils.deleteDirectory(new File(TestUtils.STATIC_SITE_PATH));
}
@ -212,6 +194,35 @@ class QApplicationJavalinServerTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAuthenticatedStaticRouter() throws Exception
{
javalinServer = new QApplicationJavalinServer(getQqqApplication())
.withServeFrontendMaterialDashboard(false)
.withPort(PORT);
javalinServer.start();
Unirest.config().setDefaultResponseEncoding("UTF-8")
.followRedirects(false);
HttpResponse<String> response = Unirest.get("http://localhost:" + PORT + "/protected-statically-served/foo.html")
.header("Authorization", "Bearer Deny")
.asString();
assertEquals(HttpStatus.FOUND.getCode(), response.getStatus());
assertThat(response.getHeaders().getFirst("Location")).contains("createMockSession");
response = Unirest.get("http://localhost:" + PORT + "/protected-statically-served/foo.html")
.asString();
assertEquals(HttpStatus.OK.getCode(), response.getStatus());
assertEquals("Foo? Bar!", response.getBody());
}
/*******************************************************************************
**
*******************************************************************************/
@ -230,6 +241,35 @@ class QApplicationJavalinServerTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAuthenticatedProcessRouter() throws Exception
{
javalinServer = new QApplicationJavalinServer(getQqqApplication())
.withServeFrontendMaterialDashboard(false)
.withPort(PORT);
javalinServer.start();
Unirest.config().setDefaultResponseEncoding("UTF-8")
.followRedirects(false);
HttpResponse<String> response = Unirest.get("http://localhost:" + PORT + "/protected-served-by-process/foo.html")
.header("Authorization", "Bearer Deny")
.asString();
assertEquals(HttpStatus.FOUND.getCode(), response.getStatus());
assertThat(response.getHeaders().getFirst("Location")).contains("createMockSession");
response = Unirest.get("http://localhost:" + PORT + "/protected-statically-served/foo.html")
.asString();
assertEquals(200, response.getStatus());
assertEquals("So you've asked for: /served-by-process/foo.html", response.getBody());
}
/***************************************************************************
**
***************************************************************************/

View File

@ -0,0 +1 @@
Foo? Bar!