From 7fae3e2329a101d5f518ce543606411cef7f3150 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 28 Dec 2022 16:52:04 -0600 Subject: [PATCH] Add table-based authentication module; update javalin to support Authentication: Basic header; Move authentication classes --- .../model/actions/AbstractActionInput.java | 2 +- .../model/metadata/QAuthenticationType.java | 1 + .../core/model/metadata/QInstance.java | 2 +- .../Auth0AuthenticationMetaData.java | 9 +- .../QAuthenticationMetaData.java | 2 +- .../TableBasedAuthenticationMetaData.java | 454 +++++++++++++ .../QAuthenticationModuleDispatcher.java | 34 +- .../Auth0AuthenticationModule.java | 14 +- .../FullyAnonymousAuthenticationModule.java | 3 +- .../MockAuthenticationModule.java | 3 +- .../TableBasedAuthenticationModule.java | 612 ++++++++++++++++++ .../memory/MemoryRecordStore.java | 12 + .../QAuthenticationModuleDispatcherTest.java | 2 +- .../Auth0AuthenticationModuleTest.java | 16 +- ...ullyAnonymousAuthenticationModuleTest.java | 2 +- .../TableBasedAuthenticationModuleTest.java | 404 ++++++++++++ .../qqq/backend/core/utils/TestUtils.java | 4 +- .../qqq/backend/module/api/TestUtils.java | 2 +- .../backend/module/filesystem/TestUtils.java | 4 +- .../qqq/backend/module/rdbms/TestUtils.java | 2 +- .../qqq/languages/javascript/TestUtils.java | 2 +- .../javalin/QJavalinImplementation.java | 58 +- ...valinImplementationAuthenticationTest.java | 224 +++++++ .../qqq/backend/javalin/QJavalinTestBase.java | 33 +- .../qqq/backend/javalin/TestUtils.java | 24 +- .../picocli/QPicoCliImplementation.java | 8 +- .../qqq/frontend/picocli/TestUtils.java | 2 +- .../sampleapp/SampleMetaDataProvider.java | 2 +- 28 files changed, 1872 insertions(+), 65 deletions(-) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/{modules/authentication/metadata => model/metadata/authentication}/Auth0AuthenticationMetaData.java (88%) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/{modules/authentication/metadata => model/metadata/authentication}/QAuthenticationMetaData.java (98%) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/TableBasedAuthenticationMetaData.java rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/{ => implementations}/Auth0AuthenticationModule.java (95%) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/{ => implementations}/FullyAnonymousAuthenticationModule.java (93%) rename qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/{ => implementations}/MockAuthenticationModule.java (94%) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java rename qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/{ => implementations}/Auth0AuthenticationModuleTest.java (95%) rename qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/{ => implementations}/FullyAnonymousAuthenticationModuleTest.java (96%) create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java create mode 100644 qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationAuthenticationTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java index 3f6e6c95..9fd77e29 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java @@ -28,8 +28,8 @@ import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java index 0550ee66..9ae00fbd 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java @@ -29,6 +29,7 @@ package com.kingsrook.qqq.backend.core.model.metadata; public enum QAuthenticationType { AUTH_0("auth0"), + TABLE_BASED("tableBased"), FULLY_ANONYMOUS("fullyAnonymous"), MOCK("mock"); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index 147ca32c..482ca631 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; @@ -48,7 +49,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.utils.StringUtils; import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvEntry; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java similarity index 88% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java index 274a4f3f..e0d46f5e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java @@ -19,11 +19,13 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.backend.core.modules.authentication.metadata; +package com.kingsrook.qqq.backend.core.model.metadata.authentication; import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule; /******************************************************************************* @@ -49,6 +51,11 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData { super(); setType(QAuthenticationType.AUTH_0); + + ////////////////////////////////////////////////////////// + // ensure this module is registered with the dispatcher // + ////////////////////////////////////////////////////////// + QAuthenticationModuleDispatcher.registerModule(QAuthenticationType.AUTH_0.getName(), Auth0AuthenticationModule.class.getName()); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/QAuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java similarity index 98% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/QAuthenticationMetaData.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java index 7e259de5..3f0b1bdb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/QAuthenticationMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.backend.core.modules.authentication.metadata; +package com.kingsrook.qqq.backend.core.model.metadata.authentication; import java.util.HashMap; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/TableBasedAuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/TableBasedAuthenticationMetaData.java new file mode 100644 index 00000000..be60a2d7 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/TableBasedAuthenticationMetaData.java @@ -0,0 +1,454 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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 . + */ + +package com.kingsrook.qqq.backend.core.model.metadata.authentication; + + +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.TableBasedAuthenticationModule; + + +/******************************************************************************* + ** Meta-data to provide details of an Auth0 Authentication module + *******************************************************************************/ +public class TableBasedAuthenticationMetaData extends QAuthenticationMetaData +{ + private String userTableName = "user"; + private String userTablePrimaryKeyField = "id"; + private String userTableUsernameField = "username"; + private String userTablePasswordHashField = "passwordHash"; + private String userTableFullNameField = "fullName"; + + private String sessionTableName = "session"; + private String sessionTablePrimaryKeyField = "id"; + private String sessionTableUuidField = "id"; + private String sessionTableUserIdField = "userId"; + private String sessionTableAccessTimestampField = "accessTimestamp"; + + private Integer inactivityTimeoutSeconds = 14_400; // 4 hours + + + + /******************************************************************************* + ** Default Constructor. + *******************************************************************************/ + public TableBasedAuthenticationMetaData() + { + super(); + setType(QAuthenticationType.TABLE_BASED); + + ////////////////////////////////////////////////////////// + // ensure this module is registered with the dispatcher // + ////////////////////////////////////////////////////////// + QAuthenticationModuleDispatcher.registerModule(QAuthenticationType.TABLE_BASED.getName(), TableBasedAuthenticationModule.class.getName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QTableMetaData defineStandardUserTable(String backendName) + { + return (new QTableMetaData() + .withName(getUserTableName()) + .withBackendName(backendName) + .withPrimaryKeyField(getUserTablePrimaryKeyField()) + .withRecordLabelFormat("%s") + .withRecordLabelFields(getUserTableUsernameField()) + .withUniqueKey(new UniqueKey(getUserTableUsernameField())) + .withField(new QFieldMetaData(getUserTablePrimaryKeyField(), QFieldType.INTEGER)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME)) + .withField(new QFieldMetaData(getUserTablePrimaryKeyField(), QFieldType.INTEGER)) + .withField(new QFieldMetaData(getUserTableUsernameField(), QFieldType.STRING)) + .withField(new QFieldMetaData(getUserTablePasswordHashField(), QFieldType.STRING)) + .withField(new QFieldMetaData(getUserTableFullNameField(), QFieldType.STRING))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QTableMetaData defineStandardSessionTable(String backendName) + { + return (new QTableMetaData() + .withName(getSessionTableName()) + .withBackendName(backendName) + .withPrimaryKeyField(getSessionTablePrimaryKeyField()) + .withRecordLabelFormat("%s") + .withRecordLabelFields(getSessionTableUuidField()) + .withUniqueKey(new UniqueKey(getSessionTableUuidField())) + .withField(new QFieldMetaData(getSessionTableUuidField(), QFieldType.STRING)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME)) + .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME)) + .withField(new QFieldMetaData(getSessionTableUserIdField(), QFieldType.INTEGER)) + .withField(new QFieldMetaData(getSessionTableAccessTimestampField(), QFieldType.DATE_TIME))); + } + + + + /******************************************************************************* + ** Getter for userTableName + *******************************************************************************/ + public String getUserTableName() + { + return (this.userTableName); + } + + + + /******************************************************************************* + ** Setter for userTableName + *******************************************************************************/ + public void setUserTableName(String userTableName) + { + this.userTableName = userTableName; + } + + + + /******************************************************************************* + ** Fluent setter for userTableName + *******************************************************************************/ + public TableBasedAuthenticationMetaData withUserTableName(String userTableName) + { + this.userTableName = userTableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for sessionTableName + *******************************************************************************/ + public String getSessionTableName() + { + return (this.sessionTableName); + } + + + + /******************************************************************************* + ** Setter for sessionTableName + *******************************************************************************/ + public void setSessionTableName(String sessionTableName) + { + this.sessionTableName = sessionTableName; + } + + + + /******************************************************************************* + ** Fluent setter for sessionTableName + *******************************************************************************/ + public TableBasedAuthenticationMetaData withSessionTableName(String sessionTableName) + { + this.sessionTableName = sessionTableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for userTablePrimaryKeyField + *******************************************************************************/ + public String getUserTablePrimaryKeyField() + { + return (this.userTablePrimaryKeyField); + } + + + + /******************************************************************************* + ** Setter for userTablePrimaryKeyField + *******************************************************************************/ + public void setUserTablePrimaryKeyField(String userTablePrimaryKeyField) + { + this.userTablePrimaryKeyField = userTablePrimaryKeyField; + } + + + + /******************************************************************************* + ** Fluent setter for userTablePrimaryKeyField + *******************************************************************************/ + public TableBasedAuthenticationMetaData withUserTablePrimaryKeyField(String userTablePrimaryKeyField) + { + this.userTablePrimaryKeyField = userTablePrimaryKeyField; + return (this); + } + + + + /******************************************************************************* + ** Getter for userTableUsernameField + *******************************************************************************/ + public String getUserTableUsernameField() + { + return (this.userTableUsernameField); + } + + + + /******************************************************************************* + ** Setter for userTableUsernameField + *******************************************************************************/ + public void setUserTableUsernameField(String userTableUsernameField) + { + this.userTableUsernameField = userTableUsernameField; + } + + + + /******************************************************************************* + ** Fluent setter for userTableUsernameField + *******************************************************************************/ + public TableBasedAuthenticationMetaData withUserTableUsernameField(String userTableUsernameField) + { + this.userTableUsernameField = userTableUsernameField; + return (this); + } + + + + /******************************************************************************* + ** Getter for sessionTablePrimaryKeyField + *******************************************************************************/ + public String getSessionTablePrimaryKeyField() + { + return (this.sessionTablePrimaryKeyField); + } + + + + /******************************************************************************* + ** Setter for sessionTablePrimaryKeyField + *******************************************************************************/ + public void setSessionTablePrimaryKeyField(String sessionTablePrimaryKeyField) + { + this.sessionTablePrimaryKeyField = sessionTablePrimaryKeyField; + } + + + + /******************************************************************************* + ** Fluent setter for sessionTablePrimaryKeyField + *******************************************************************************/ + public TableBasedAuthenticationMetaData withSessionTablePrimaryKeyField(String sessionTablePrimaryKeyField) + { + this.sessionTablePrimaryKeyField = sessionTablePrimaryKeyField; + return (this); + } + + + + /******************************************************************************* + ** Getter for sessionTableUserIdField + *******************************************************************************/ + public String getSessionTableUserIdField() + { + return (this.sessionTableUserIdField); + } + + + + /******************************************************************************* + ** Setter for sessionTableUserIdField + *******************************************************************************/ + public void setSessionTableUserIdField(String sessionTableUserIdField) + { + this.sessionTableUserIdField = sessionTableUserIdField; + } + + + + /******************************************************************************* + ** Fluent setter for sessionTableUserIdField + *******************************************************************************/ + public TableBasedAuthenticationMetaData withSessionTableUserIdField(String sessionTableUserIdField) + { + this.sessionTableUserIdField = sessionTableUserIdField; + return (this); + } + + + + /******************************************************************************* + ** Getter for sessionTableUuidField + *******************************************************************************/ + public String getSessionTableUuidField() + { + return (this.sessionTableUuidField); + } + + + + /******************************************************************************* + ** Setter for sessionTableUuidField + *******************************************************************************/ + public void setSessionTableUuidField(String sessionTableUuidField) + { + this.sessionTableUuidField = sessionTableUuidField; + } + + + + /******************************************************************************* + ** Fluent setter for sessionTableUuidField + *******************************************************************************/ + public TableBasedAuthenticationMetaData withSessionTableUuidField(String sessionTableUuidField) + { + this.sessionTableUuidField = sessionTableUuidField; + return (this); + } + + + + /******************************************************************************* + ** Getter for userTableFullNameField + *******************************************************************************/ + public String getUserTableFullNameField() + { + return (this.userTableFullNameField); + } + + + + /******************************************************************************* + ** Setter for userTableFullNameField + *******************************************************************************/ + public void setUserTableFullNameField(String userTableFullNameField) + { + this.userTableFullNameField = userTableFullNameField; + } + + + + /******************************************************************************* + ** Fluent setter for userTableFullNameField + *******************************************************************************/ + public TableBasedAuthenticationMetaData withUserTableFullNameField(String userTableFullNameField) + { + this.userTableFullNameField = userTableFullNameField; + return (this); + } + + + + /******************************************************************************* + ** Getter for userTablePasswordHashField + *******************************************************************************/ + public String getUserTablePasswordHashField() + { + return (this.userTablePasswordHashField); + } + + + + /******************************************************************************* + ** Setter for userTablePasswordHashField + *******************************************************************************/ + public void setUserTablePasswordHashField(String userTablePasswordHashField) + { + this.userTablePasswordHashField = userTablePasswordHashField; + } + + + + /******************************************************************************* + ** Fluent setter for userTablePasswordHashField + *******************************************************************************/ + public TableBasedAuthenticationMetaData withUserTablePasswordHashField(String userTablePasswordHashField) + { + this.userTablePasswordHashField = userTablePasswordHashField; + return (this); + } + + + + /******************************************************************************* + ** Getter for sessionTableAccessTimestampField + *******************************************************************************/ + public String getSessionTableAccessTimestampField() + { + return (this.sessionTableAccessTimestampField); + } + + + + /******************************************************************************* + ** Setter for sessionTableAccessTimestampField + *******************************************************************************/ + public void setSessionTableAccessTimestampField(String sessionTableAccessTimestampField) + { + this.sessionTableAccessTimestampField = sessionTableAccessTimestampField; + } + + + + /******************************************************************************* + ** Fluent setter for sessionTableAccessTimestampField + *******************************************************************************/ + public TableBasedAuthenticationMetaData withSessionTableAccessTimestampField(String sessionTableAccessTimestampField) + { + this.sessionTableAccessTimestampField = sessionTableAccessTimestampField; + return (this); + } + + + + /******************************************************************************* + ** Getter for inactivityTimeoutSeconds + *******************************************************************************/ + public Integer getInactivityTimeoutSeconds() + { + return (this.inactivityTimeoutSeconds); + } + + + + /******************************************************************************* + ** Setter for inactivityTimeoutSeconds + *******************************************************************************/ + public void setInactivityTimeoutSeconds(Integer inactivityTimeoutSeconds) + { + this.inactivityTimeoutSeconds = inactivityTimeoutSeconds; + } + + + + /******************************************************************************* + ** Fluent setter for inactivityTimeoutSeconds + *******************************************************************************/ + public TableBasedAuthenticationMetaData withInactivityTimeoutSeconds(Integer inactivityTimeoutSeconds) + { + this.inactivityTimeoutSeconds = inactivityTimeoutSeconds; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcher.java index 258509f2..8d925847 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcher.java @@ -22,11 +22,14 @@ package com.kingsrook.qqq.backend.core.modules.authentication; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.FullyAnonymousAuthenticationModule; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.MockAuthenticationModule; /******************************************************************************* @@ -38,20 +41,35 @@ import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthentic *******************************************************************************/ public class QAuthenticationModuleDispatcher { - private final Map authenticationTypeToModuleClassNameMap; - + private static Map authenticationTypeToModuleClassNameMap = Collections.synchronizedMap(new HashMap<>()); + static + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // ensure our 2 default modules are registered. // + // Note that for "real" implementations, the pattern is for their MetaData class's constructor to register. // + // the idea being, any qInstance using such a module, surely will have some MetaData defined for it. // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + registerModule(QAuthenticationType.MOCK.getName(), MockAuthenticationModule.class.getName()); + registerModule(QAuthenticationType.FULLY_ANONYMOUS.getName(), FullyAnonymousAuthenticationModule.class.getName()); + } /******************************************************************************* ** *******************************************************************************/ public QAuthenticationModuleDispatcher() { - authenticationTypeToModuleClassNameMap = new HashMap<>(); - authenticationTypeToModuleClassNameMap.put(QAuthenticationType.MOCK.getName(), "com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule"); - authenticationTypeToModuleClassNameMap.put(QAuthenticationType.FULLY_ANONYMOUS.getName(), "com.kingsrook.qqq.backend.core.modules.authentication.FullyAnonymousAuthenticationModule"); - authenticationTypeToModuleClassNameMap.put(QAuthenticationType.AUTH_0.getName(), "com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule"); - // todo - let user define custom type -> classes + + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void registerModule(String name, String className) + { + authenticationTypeToModuleClassNameMap.putIfAbsent(name, className); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java similarity index 95% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java index aaa9bc36..f5264730 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.backend.core.modules.authentication; +package com.kingsrook.qqq.backend.core.modules.authentication.implementations; import java.nio.charset.StandardCharsets; @@ -45,9 +45,10 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.JWTVerifier; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData; 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.metadata.Auth0AuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; import com.kingsrook.qqq.backend.core.state.AbstractStateKey; import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; import com.kingsrook.qqq.backend.core.state.StateProviderInterface; @@ -93,15 +94,15 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface if(context.containsKey(BASIC_AUTH_KEY)) { Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication(); - AuthAPI auth = new AuthAPI(metaData.getBaseUrl(), metaData.getClientId(), metaData.getClientSecret()); + AuthAPI auth = new AuthAPI(metaData.getBaseUrl(), metaData.getClientId(), metaData.getClientSecret()); try { ///////////////////////////////////////////////// // decode the credentials from the header auth // ///////////////////////////////////////////////// String base64Credentials = context.get(BASIC_AUTH_KEY).trim(); - byte[] credDecoded = Base64.getDecoder().decode(base64Credentials); - String credentials = new String(credDecoded, StandardCharsets.UTF_8); + byte[] credDecoded = Base64.getDecoder().decode(base64Credentials); + String credentials = new String(credDecoded, StandardCharsets.UTF_8); ///////////////////////////////////// // call auth0 with a login request // @@ -117,11 +118,10 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface //////////////// // ¯\_(ツ)_/¯ // //////////////// - String message = "An unknown error occurred during handling basic auth"; + String message = "Error handling basic authentication: " + e.getMessage(); LOG.error(message, e); throw (new QAuthenticationException(message)); } - } ////////////////////////////////////////////////// diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java similarity index 93% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java index 8a0e7078..01f582d7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.backend.core.modules.authentication; +package com.kingsrook.qqq.backend.core.modules.authentication.implementations; import java.util.Map; @@ -27,6 +27,7 @@ import java.util.UUID; 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.QUser; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; /******************************************************************************* diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java similarity index 94% rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java index f490d0a4..afc2cc39 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.backend.core.modules.authentication; +package com.kingsrook.qqq.backend.core.modules.authentication.implementations; import java.util.Map; @@ -27,6 +27,7 @@ import java.util.UUID; 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.QUser; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java new file mode 100644 index 00000000..14202fbc --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java @@ -0,0 +1,612 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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 . + */ + +package com.kingsrook.qqq.backend.core.modules.authentication.implementations; + + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; +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.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.TableBasedAuthenticationMetaData; +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.QAuthenticationModuleInterface; +import com.kingsrook.qqq.backend.core.state.AbstractStateKey; +import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; +import com.kingsrook.qqq.backend.core.state.StateProviderInterface; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class TableBasedAuthenticationModule implements QAuthenticationModuleInterface +{ + private static final Logger LOG = LogManager.getLogger(TableBasedAuthenticationModule.class); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // 30 minutes - ideally this would be lower, but right now we've been dealing with re-validation issues... // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + public static final int ID_TOKEN_VALIDATION_INTERVAL_SECONDS = 1800; + + public static final String SESSION_ID_KEY = "sessionId"; + public static final String BASIC_AUTH_KEY = "basicAuthString"; + + public static final String SESSION_ID_NOT_PROVIDED_ERROR = "Session ID was not provided"; + + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this is how we allow the actions within this class to work without themselves having a logged-in user. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + private static QSession chickenAndEggSession = new QSession() + { + + }; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException + { + TableBasedAuthenticationMetaData metaData = (TableBasedAuthenticationMetaData) qInstance.getAuthentication(); + String sessionUuid = context.get(SESSION_ID_KEY); + + /////////////////////////////////////////////////////////// + // check if we are processing a Basic Auth Session first // + /////////////////////////////////////////////////////////// + if(context.containsKey(BASIC_AUTH_KEY)) + { + try + { + ///////////////////////////////////////////////// + // decode the credentials from the header auth // + ///////////////////////////////////////////////// + String base64Credentials = context.get(BASIC_AUTH_KEY).trim(); + byte[] credDecoded = Base64.getDecoder().decode(base64Credentials); + String credentials = new String(credDecoded, StandardCharsets.UTF_8); + + /////////////////////////// + // fetch the user record // + /////////////////////////// + GetInput getInput = new GetInput(qInstance); + getInput.setSession(chickenAndEggSession); + getInput.setTableName(metaData.getUserTableName()); + getInput.setUniqueKey(Map.of(metaData.getUserTableUsernameField(), credentials.split(":")[0])); + GetOutput getOutput = new GetAction().execute(getInput); + if(getOutput.getRecord() == null) + { + throw (new QAuthenticationException("Incorrect username or password.")); + } + + ////////////////////////////////////////////////////////// + // compare the hashed input password to the stored hash // + ////////////////////////////////////////////////////////// + QRecord user = getOutput.getRecord(); + String inputPassword = credentials.split(":")[1]; + String storedHash = user.getValueString(metaData.getUserTablePasswordHashField()); + + // if(!inputHash.equals(storedHash)) + if(!PasswordHasher.validatePassword(inputPassword, storedHash)) + { + throw (new QAuthenticationException("Incorrect username or password.")); + } + + ////////////////////// + // insert a session // + ////////////////////// + sessionUuid = UUID.randomUUID().toString(); + QRecord sessionRecord = new QRecord() + .withValue(metaData.getSessionTableUuidField(), sessionUuid) + .withValue(metaData.getSessionTableAccessTimestampField(), Instant.now()) + .withValue(metaData.getSessionTableUserIdField(), user.getValue(metaData.getUserTablePrimaryKeyField())); + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(chickenAndEggSession); + insertInput.setTableName(metaData.getSessionTableName()); + insertInput.setRecords(List.of(sessionRecord)); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + if(CollectionUtils.nullSafeHasContents(insertOutput.getRecords().get(0).getErrors())) + { + LOG.warn("Inserting session failed: " + insertOutput.getRecords().get(0).getErrors()); + throw (new QAuthenticationException("Incorrect username or password.")); + } + } + catch(QAuthenticationException ae) + { + // todo - sleep to obscure what was the issue. + throw (ae); + } + catch(Exception e) + { + //////////////// + // ¯\_(ツ)_/¯ // + //////////////// + // todo - sleep to obscure what was the issue. + String message = "Error handling basic authentication: " + e.getMessage(); + LOG.error(message, e); + throw (new QAuthenticationException(message)); + } + } + + ////////////////////////////////////////////////// + // get the session uuid from the context object // + ////////////////////////////////////////////////// + if(sessionUuid == null) + { + LOG.warn(SESSION_ID_NOT_PROVIDED_ERROR); + throw (new QAuthenticationException(SESSION_ID_NOT_PROVIDED_ERROR)); + } + + try + { + ///////////////////////////////////////////////////// + // try to build session to see if still valid // + // then call method to check more session validity // + ///////////////////////////////////////////////////// + QSession qSession = buildQSessionFromUuid(qInstance, metaData, sessionUuid); + if(isSessionValid(qInstance, qSession)) + { + return (qSession); + } + + /////////////////////////////////////////////////////////////////////////////////////// + // if we make it here it means we have never validated this token or its been a long // + // enough duration so we need to re-verify the token // + /////////////////////////////////////////////////////////////////////////////////////// + qSession = revalidateSession(qInstance, sessionUuid); + + //////////////////////////////////////////////////////////////////// + // put now into state so we dont check until next interval passes // + /////////////////////////////////////////////////////////////////// + StateProviderInterface spi = getStateProvider(); + SessionIdStateKey key = new SessionIdStateKey(qSession.getIdReference()); + spi.put(key, Instant.now()); + + return (qSession); + } + catch(QAuthenticationException ae) + { + LOG.info("Authentication exception", ae); + throw (ae); + } + catch(Exception e) + { + //////////////// + // ¯\_(ツ)_/¯ // + //////////////// + String message = "An unknown error occurred"; + LOG.error(message, e); + throw (new QAuthenticationException(message)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean isSessionValid(QInstance instance, QSession session) + { + if(session == chickenAndEggSession) + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this is how we allow the actions within this class to work without themselves having a logged-in user. // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + return (true); + } + + if(session == null) + { + return (false); + } + + if(session.getIdReference() == null) + { + return (false); + } + + StateProviderInterface stateProvider = getStateProvider(); + SessionIdStateKey key = new SessionIdStateKey(session.getIdReference()); + Optional lastTimeCheckedOptional = stateProvider.get(Instant.class, key); + if(lastTimeCheckedOptional.isPresent()) + { + Instant lastTimeChecked = lastTimeCheckedOptional.get(); + + /////////////////////////////////////////////////////////////////////////////////////////////////// + // returns negative int if less than compared duration, 0 if equal, positive int if greater than // + // - so this is basically saying, if the time between the last time we checked the token and // + // right now is more than ID_TOKEN_VALIDATION_INTERVAL_SECTIONS, then session needs revalidated // + /////////////////////////////////////////////////////////////////////////////////////////////////// + if(Duration.between(lastTimeChecked, Instant.now()).compareTo(Duration.ofSeconds(ID_TOKEN_VALIDATION_INTERVAL_SECONDS)) < 0) + { + return (true); + } + } + + try + { + LOG.debug("Re-validating token due to validation interval [" + lastTimeCheckedOptional + "] being passed (or never being set): " + session.getIdReference()); + revalidateSession(instance, session.getIdReference()); + + ////////////////////////////////////////////////////////////////// + // update the timestamp in state provider, to avoid re-checking // + ////////////////////////////////////////////////////////////////// + stateProvider.put(key, Instant.now()); + + return (true); + } + catch(QAuthenticationException ae) + { + return (false); + } + catch(Exception e) + { + LOG.warn("Error validating session", e); + return (false); + } + } + + + + /******************************************************************************* + ** makes request to check if a session is still valid and build new qSession if it is + ** + *******************************************************************************/ + private QSession revalidateSession(QInstance qInstance, String sessionUuid) throws QException + { + TableBasedAuthenticationMetaData metaData = (TableBasedAuthenticationMetaData) qInstance.getAuthentication(); + + GetInput getSessionInput = new GetInput(qInstance); + getSessionInput.setSession(chickenAndEggSession); + getSessionInput.setTableName(metaData.getSessionTableName()); + getSessionInput.setUniqueKey(Map.of(metaData.getSessionTableUuidField(), sessionUuid)); + GetOutput getSessionOutput = new GetAction().execute(getSessionInput); + if(getSessionOutput.getRecord() == null) + { + throw (new QAuthenticationException("Session not found.")); + } + QRecord sessionRecord = getSessionOutput.getRecord(); + Instant lastAccess = sessionRecord.getValueInstant(metaData.getSessionTableAccessTimestampField()); + + /////////////////////////////////////////////////////////////////////////////////////////////////// + // returns negative int if less than compared duration, 0 if equal, positive int if greater than // + // - so this is basically saying, if the time between the last time the session was marked as // + // active, and right now is more than the timeout seconds, then the session is expired // + /////////////////////////////////////////////////////////////////////////////////////////////////// + if(lastAccess.plus(Duration.ofSeconds(metaData.getInactivityTimeoutSeconds())).isBefore(Instant.now())) + { + throw (new QAuthenticationException("Session is expired.")); + } + + /////////////////////////////////////////////// + // update the timestamp in the session table // + /////////////////////////////////////////////// + UpdateInput updateInput = new UpdateInput(qInstance); + updateInput.setSession(chickenAndEggSession); + updateInput.setTableName(metaData.getSessionTableName()); + updateInput.setRecords(List.of(new QRecord() + .withValue(metaData.getSessionTablePrimaryKeyField(), sessionRecord.getValue(metaData.getSessionTablePrimaryKeyField())) + .withValue(metaData.getSessionTableAccessTimestampField(), Instant.now()))); + new UpdateAction().execute(updateInput); + + return (buildQSessionFromUuid(qInstance, metaData, sessionUuid)); + } + + + + /******************************************************************************* + ** extracts info from token creating a QSession + ** + *******************************************************************************/ + private QSession buildQSessionFromUuid(QInstance qInstance, TableBasedAuthenticationMetaData metaData, String sessionUuid) throws QException + { + GetInput getSessionInput = new GetInput(qInstance); + getSessionInput.setSession(chickenAndEggSession); + getSessionInput.setTableName(metaData.getSessionTableName()); + getSessionInput.setUniqueKey(Map.of(metaData.getSessionTableUuidField(), sessionUuid)); + GetOutput getSessionOutput = new GetAction().execute(getSessionInput); + if(getSessionOutput.getRecord() == null) + { + throw (new QAuthenticationException("Session not found.")); + } + QRecord sessionRecord = getSessionOutput.getRecord(); + + GetInput getUserInput = new GetInput(qInstance); + getUserInput.setSession(chickenAndEggSession); + getUserInput.setTableName(metaData.getUserTableName()); + getUserInput.setPrimaryKey(sessionRecord.getValue(metaData.getSessionTableUserIdField())); + GetOutput getUserOutput = new GetAction().execute(getUserInput); + if(getUserOutput.getRecord() == null) + { + throw (new QAuthenticationException("User for session not found.")); + } + QRecord userRecord = getUserOutput.getRecord(); + + QUser qUser = new QUser(); + qUser.setFullName(userRecord.getValueString(metaData.getUserTableFullNameField())); + qUser.setIdReference(userRecord.getValueString(metaData.getUserTableUsernameField())); + + QSession qSession = new QSession(); + qSession.setIdReference(sessionUuid); + qSession.setUser(qUser); + + return (qSession); + } + + + + /******************************************************************************* + ** Load an instance of the appropriate state provider + ** + *******************************************************************************/ + public static StateProviderInterface getStateProvider() + { + // TODO - read this from somewhere in meta data eh? + return (InMemoryStateProvider.getInstance()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class SessionIdStateKey extends AbstractStateKey + { + private final String key; + + + + /******************************************************************************* + ** Constructor. + ** + *******************************************************************************/ + SessionIdStateKey(String key) + { + this.key = key; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + return (this.key); + } + + + + /******************************************************************************* + ** Make the key give a unique string to identify itself. + * + *******************************************************************************/ + @Override + public String getUniqueIdentifier() + { + return (this.key); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean equals(Object o) + { + if(this == o) + { + return true; + } + if(o == null || getClass() != o.getClass()) + { + return false; + } + SessionIdStateKey that = (SessionIdStateKey) o; + return key.equals(that.key); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int hashCode() + { + return key.hashCode(); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static class PasswordHasher + { + private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1"; + private static final int SALT_BYTE_SIZE = 32; + private static final int HASH_BYTE_SIZE = 32; + private static final int PBKDF2_ITERATIONS = 1000; + + + + /******************************************************************************* + ** Returns a salted, hashed version of a raw password. + ** + *******************************************************************************/ + public static String createHashedPassword(String password) throws NoSuchAlgorithmException, InvalidKeySpecException + { + //////////////////////////// + // Generate a random salt // + //////////////////////////// + SecureRandom random = new SecureRandom(); + byte[] salt = new byte[SALT_BYTE_SIZE]; + random.nextBytes(salt); + + /////////////////////// + // Hash the password // + /////////////////////// + byte[] passwordHash = computePbkdf2Hash(password.toCharArray(), salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE); + + ////////////////////////////////////////////////////// + // return string in the format iterations:salt:hash // + ////////////////////////////////////////////////////// + return (PBKDF2_ITERATIONS + ":" + toHex(salt) + ":" + toHex(passwordHash)); + } + + + + /******************************************************************************* + ** Computes the PBKDF2 hash. + ** + *******************************************************************************/ + private static byte[] computePbkdf2Hash(char[] password, byte[] salt, int iterations, int bytes) throws NoSuchAlgorithmException, InvalidKeySpecException + { + PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8); + SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM); + + return skf.generateSecret(spec).getEncoded(); + } + + + + /******************************************************************************* + ** Thanks to Baeldung for this and related methods + ** https://www.baeldung.com/java-byte-arrays-hex-strings + *******************************************************************************/ + private static String toHex(byte[] array) + { + StringBuilder hexStringBuffer = new StringBuilder(); + for(byte b : array) + { + hexStringBuffer.append(Character.forDigit((b >> 4) & 0xF, 16)); + hexStringBuffer.append(Character.forDigit((b & 0xF), 16)); + } + return hexStringBuffer.toString(); + } + + + + /******************************************************************************* + ** Validates a password against a hash. + ** + *******************************************************************************/ + private static boolean validatePassword(String password, String passwordHash) throws NoSuchAlgorithmException, InvalidKeySpecException + { + String[] params = passwordHash.split(":"); + int iterations = Integer.parseInt(params[0]); + byte[] salt = fromHex(params[1]); + byte[] hash = fromHex(params[2]); + + byte[] testHash = computePbkdf2Hash(password.toCharArray(), salt, iterations, hash.length); + return slowEquals(hash, testHash); + } + + + + /******************************************************************************* + ** Compares two byte arrays in length-constant time. This comparison method + ** is used so that password hashes cannot be extracted from an on-line + ** system using a timing attack and then attacked off-line. + ** + *******************************************************************************/ + private static boolean slowEquals(byte[] a, byte[] b) + { + int diff = a.length ^ b.length; + + for(int i = 0; i < a.length && i < b.length; i++) + { + diff |= a[i] ^ b[i]; + } + + return diff == 0; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static int toHexDigit(char hexChar) + { + int digit = Character.digit(hexChar, 16); + if(digit == -1) + { + throw new IllegalArgumentException("Invalid Hexadecimal Character: " + hexChar); + } + return digit; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static byte[] fromHex(String hexString) + { + if(hexString.length() % 2 == 1) + { + throw new IllegalArgumentException("Invalid hexadecimal String supplied."); + } + + byte[] bytes = new byte[hexString.length() / 2]; + for(int i = 0; i < hexString.length(); i += 2) + { + int firstDigit = toHexDigit(hexString.charAt(i)); + int secondDigit = toHexDigit(hexString.charAt(i + 1)); + bytes[i / 2] = (byte) ((firstDigit << 4) + secondDigit); + } + return bytes; + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index 11940619..be82ec82 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -74,6 +74,18 @@ public class MemoryRecordStore + /******************************************************************************* + ** forget all data AND statistics + *******************************************************************************/ + public static void fullReset() + { + getInstance().reset(); + resetStatistics(); + setCollectStatistics(false); + } + + + /******************************************************************************* ** Forget all data in the memory store... *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcherTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcherTest.java index 44a0ae3b..f498bfe7 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcherTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcherTest.java @@ -23,7 +23,7 @@ package com.kingsrook.qqq.backend.core.modules.authentication; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java similarity index 95% rename from qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java rename to qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java index bc69be9d..b2992cbe 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.backend.core.modules.authentication; +package com.kingsrook.qqq.backend.core.modules.authentication.implementations; import java.time.Instant; @@ -28,17 +28,17 @@ import java.util.HashMap; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.session.QSession; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.Auth0AuthenticationMetaData; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; -import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.AUTH0_ID_TOKEN_KEY; -import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.COULD_NOT_DECODE_ERROR; -import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.EXPIRED_TOKEN_ERROR; -import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.INVALID_TOKEN_ERROR; -import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.TOKEN_NOT_PROVIDED_ERROR; +import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.AUTH0_ID_TOKEN_KEY; +import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.COULD_NOT_DECODE_ERROR; +import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.EXPIRED_TOKEN_ERROR; +import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.INVALID_TOKEN_ERROR; +import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.TOKEN_NOT_PROVIDED_ERROR; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModuleTest.java similarity index 96% rename from qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java rename to qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModuleTest.java index f775511f..5785bc93 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModuleTest.java @@ -19,7 +19,7 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.backend.core.modules.authentication; +package com.kingsrook.qqq.backend.core.modules.authentication.implementations; import com.kingsrook.qqq.backend.core.model.session.QSession; diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java new file mode 100644 index 00000000..cd0976e2 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java @@ -0,0 +1,404 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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 . + */ + +package com.kingsrook.qqq.backend.core.modules.authentication.implementations; + + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; +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.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.TableBasedAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; +import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +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.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for the TableBasedAuthenticationModule + *******************************************************************************/ +public class TableBasedAuthenticationModuleTest +{ + public static final String USERNAME = "jdoe"; + public static final String PASSWORD = "abc123"; + public static final String FULL_NAME = "John Doe"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + @AfterEach + void beforeAndAfterEach() + { + MemoryRecordStore.getInstance().reset(); + MemoryRecordStore.resetStatistics(); + MemoryRecordStore.setCollectStatistics(false); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSuccessfulLogin() throws Exception + { + QInstance qInstance = getQInstance(); + insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME); + + QSession session = new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.BASIC_AUTH_KEY, encodeBasicAuth(USERNAME, PASSWORD))); + + assertNotNull(session); + assertNotNull(session.getIdReference()); + assertNotNull(session.getUser()); + assertEquals(USERNAME, session.getUser().getIdReference()); + assertEquals(FULL_NAME, session.getUser().getFullName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testBadUsernameAndPassword() throws Exception + { + QInstance qInstance = getQInstance(); + insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME); + + TableBasedAuthenticationModule authModule = new TableBasedAuthenticationModule(); + + assertThatThrownBy(() -> authModule.createSession(qInstance, Map.of(TableBasedAuthenticationModule.BASIC_AUTH_KEY, encodeBasicAuth("not-" + USERNAME, PASSWORD)))) + .isInstanceOf(QAuthenticationException.class) + .hasMessageContaining("Incorrect username or password"); + + assertThatThrownBy(() -> authModule.createSession(qInstance, Map.of(TableBasedAuthenticationModule.BASIC_AUTH_KEY, encodeBasicAuth(USERNAME, "not-" + PASSWORD)))) + .isInstanceOf(QAuthenticationException.class) + .hasMessageContaining("Incorrect username or password"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNoContextProvided() throws Exception + { + QInstance qInstance = getQInstance(); + insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME); + + assertThatThrownBy(() -> new TableBasedAuthenticationModule().createSession(qInstance, Collections.emptyMap())) + .isInstanceOf(QAuthenticationException.class) + .hasMessageContaining("Session ID was not provided"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testUseExistingSession() throws Exception + { + QInstance qInstance = getQInstance(); + + insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME); + String uuid = insertTestSession(qInstance, USERNAME, Instant.now()); + + QSession session = new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, uuid)); + assertNotNull(session); + assertEquals(uuid, session.getIdReference()); + assertNotNull(session.getUser()); + assertEquals(USERNAME, session.getUser().getIdReference()); + assertEquals(FULL_NAME, session.getUser().getFullName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCreatingAlmostExpiredSession() throws Exception + { + QInstance qInstance = getQInstance(); + + insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME); + String uuid = insertTestSession(qInstance, USERNAME, Instant.now().minus(4, ChronoUnit.HOURS).plus(1, ChronoUnit.MINUTES)); + + QSession session = new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, uuid)); + assertNotNull(session); + assertEquals(uuid, session.getIdReference()); + assertNotNull(session.getUser()); + assertEquals(USERNAME, session.getUser().getIdReference()); + assertEquals(FULL_NAME, session.getUser().getFullName()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testValidatingAlmostExpiredSession() throws Exception + { + QInstance qInstance = getQInstance(); + + insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME); + String uuid = insertTestSession(qInstance, USERNAME, Instant.now().minus(4, ChronoUnit.HOURS).plus(1, ChronoUnit.MINUTES)); + + QSession session = new QSession(); + session.setIdReference(uuid); + InMemoryStateProvider.getInstance().put(new TableBasedAuthenticationModule.SessionIdStateKey(session.getIdReference()), Instant.now()); + assertTrue(new TableBasedAuthenticationModule().isSessionValid(qInstance, session)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCreatingJustExpiredSession() throws Exception + { + QInstance qInstance = getQInstance(); + + insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME); + String uuid = insertTestSession(qInstance, USERNAME, Instant.now().minus(4, ChronoUnit.HOURS).minus(1, ChronoUnit.MINUTES)); + + assertThatThrownBy(() -> new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, uuid))) + .isInstanceOf(QAuthenticationException.class) + .hasMessageContaining("Session is expired"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testValidatingJustExpiredSession() throws Exception + { + QInstance qInstance = getQInstance(); + + insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME); + String uuid = insertTestSession(qInstance, USERNAME, Instant.now().minus(4, ChronoUnit.HOURS).minus(1, ChronoUnit.MINUTES)); + + QSession session = new QSession(); + session.setIdReference(uuid); + assertFalse(new TableBasedAuthenticationModule().isSessionValid(qInstance, session)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testValidatingNullInputs() + { + assertFalse(new TableBasedAuthenticationModule().isSessionValid(getQInstance(), null)); + assertFalse(new TableBasedAuthenticationModule().isSessionValid(getQInstance(), new QSession())); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNonExistingSessionUUID() throws Exception + { + QInstance qInstance = getQInstance(); + + insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME); + String uuid = insertTestSession(qInstance, USERNAME, Instant.now()); + + assertThatThrownBy(() -> new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, "not-" + uuid))) + .isInstanceOf(QAuthenticationException.class) + .hasMessageContaining("Session not found"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testExistingSessionWithBadUserId() throws Exception + { + QInstance qInstance = getQInstance(); + + insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME); + String uuid = insertTestSession(qInstance, "not-" + USERNAME, Instant.now()); + + assertThatThrownBy(() -> new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, uuid))) + .isInstanceOf(QAuthenticationException.class) + .hasMessageContaining("User for session not found"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWeDontAlwaysRevalidate() throws Exception + { + QInstance qInstance = getQInstance(); + + insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME); + String uuid = insertTestSession(qInstance, USERNAME, Instant.now().minus(4, ChronoUnit.HOURS).plus(1, ChronoUnit.MINUTES)); + + QSession session = new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, uuid)); + + MemoryRecordStore.setCollectStatistics(true); + + assertTrue(new TableBasedAuthenticationModule().isSessionValid(qInstance, session)); + Map statistics = MemoryRecordStore.getStatistics(); + assertEquals(0, statistics.size()); // should be no stats of any type! + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWeDoAlwaysRevalidateIfNeeded() throws Exception + { + QInstance qInstance = getQInstance(); + + insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME); + String uuid = insertTestSession(qInstance, USERNAME, Instant.now().minus(4, ChronoUnit.HOURS).plus(1, ChronoUnit.MINUTES)); + + QSession session = new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, uuid)); + + assertTrue(new TableBasedAuthenticationModule().isSessionValid(qInstance, session)); + + InMemoryStateProvider.getInstance().put(new TableBasedAuthenticationModule.SessionIdStateKey(session.getIdReference()), Instant.now().minus(TableBasedAuthenticationModule.ID_TOKEN_VALIDATION_INTERVAL_SECONDS + 10, ChronoUnit.SECONDS)); + + MemoryRecordStore.setCollectStatistics(true); + assertTrue(new TableBasedAuthenticationModule().isSessionValid(qInstance, session)); + Map statistics = MemoryRecordStore.getStatistics(); + assertEquals(3, statistics.get(MemoryRecordStore.STAT_QUERIES_RAN)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void insertTestUser(QInstance qInstance, String username, String password, String fullName) throws Exception + { + QAuthenticationMetaData tableBasedAuthentication = qInstance.getAuthentication(); + qInstance.setAuthentication(new Auth0AuthenticationMetaData().withName("mock").withType(QAuthenticationType.MOCK)); + TestUtils.insertRecords(qInstance, qInstance.getTable("user"), List.of(new QRecord() + .withValue("username", username) + .withValue("fullName", fullName) + .withValue("passwordHash", TableBasedAuthenticationModule.PasswordHasher.createHashedPassword(password)))); + qInstance.setAuthentication(tableBasedAuthentication); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static String insertTestSession(QInstance qInstance, String username, Instant accessTimestamp) throws Exception + { + QAuthenticationMetaData tableBasedAuthentication = qInstance.getAuthentication(); + qInstance.setAuthentication(new Auth0AuthenticationMetaData().withName("mock").withType(QAuthenticationType.MOCK)); + + String uuid = UUID.randomUUID().toString(); + + GetInput getUserInput = new GetInput(qInstance); + getUserInput.setSession(new QSession()); + getUserInput.setTableName("user"); + getUserInput.setUniqueKey(Map.of("username", username)); + GetOutput getUserOutput = new GetAction().execute(getUserInput); + + TestUtils.insertRecords(qInstance, qInstance.getTable("session"), List.of(new QRecord() + .withValue("id", uuid) + .withValue("userId", getUserOutput.getRecord() == null ? -1 : getUserOutput.getRecord().getValueInteger("id")) + .withValue("accessTimestamp", accessTimestamp) + .withValue("passwordHash", TableBasedAuthenticationModule.PasswordHasher.createHashedPassword(PASSWORD)))); + + qInstance.setAuthentication(tableBasedAuthentication); + + return (uuid); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String encodeBasicAuth(String username, String password) + { + Base64.Encoder encoder = Base64.getEncoder(); + String originalString = username + ":" + password; + return (encoder.encodeToString(originalString.getBytes())); + } + + + + /******************************************************************************* + ** utility method to prime a qInstance for these tests + ** + *******************************************************************************/ + private QInstance getQInstance() + { + TableBasedAuthenticationMetaData authenticationMetaData = new TableBasedAuthenticationMetaData(); + + QInstance qInstance = TestUtils.defineInstance(); + qInstance.setAuthentication(authenticationMetaData); + qInstance.addTable(authenticationMetaData.defineStandardUserTable(TestUtils.MEMORY_BACKEND_NAME)); + qInstance.addTable(authenticationMetaData.defineStandardSessionTable(TestUtils.MEMORY_BACKEND_NAME)); + + return (qInstance); + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 68d8cbe8..2178c865 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -95,8 +95,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEv import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf; import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase; import com.kingsrook.qqq.backend.core.model.session.QSession; -import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.MockAuthenticationModule; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule; import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration; diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java index 5388297f..cf358633 100644 --- a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java +++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java @@ -31,7 +31,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.module.api.model.AuthorizationType; import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData; import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails; diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/TestUtils.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/TestUtils.java index 3b174399..ef53bb8b 100644 --- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/TestUtils.java +++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/TestUtils.java @@ -37,8 +37,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; 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.MockAuthenticationModule; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.MockAuthenticationModule; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess; import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.Cardinality; import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.RecordFormat; diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java index a5470939..84aff27d 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java @@ -36,7 +36,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueForm import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSActionTest; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; diff --git a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java index 8d414889..eb29781a 100644 --- a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java +++ b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java @@ -28,7 +28,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; /******************************************************************************* diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index f4783a6a..aa723c23 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -88,7 +88,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.Auth0AuthenticationModule; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -122,6 +121,7 @@ public class QJavalinImplementation private static final int SESSION_COOKIE_AGE = 60 * 60 * 24; private static final String SESSION_ID_COOKIE_NAME = "sessionId"; + private static final String BASIC_AUTH_NAME = "basicAuthString"; static QInstance qInstance; static QJavalinMetaData javalinMetaData = new QJavalinMetaData(); @@ -326,35 +326,55 @@ public class QJavalinImplementation /******************************************************************************* ** *******************************************************************************/ - static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException + static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException, QAuthenticationException { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData()); + boolean needToSetSessionIdCookie = false; try { Map authenticationContext = new HashMap<>(); - ///////////////////////////////////////////////////////////////////////////////// - // look for a token in either the sessionId cookie, or an Authorization header // - ///////////////////////////////////////////////////////////////////////////////// - String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME); + String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME); + String authorizationHeaderValue = context.header("Authorization"); + if(StringUtils.hasContent(sessionIdCookieValue)) { + //////////////////////////////////////// + // first, look for a sessionId cookie // + //////////////////////////////////////// authenticationContext.put(SESSION_ID_COOKIE_NAME, sessionIdCookieValue); + needToSetSessionIdCookie = true; + } + else if(authorizationHeaderValue != null) + { + ///////////////////////////////////////////////////////////////////////////////////////////////// + // second, look for the authorization header: // + // either with a "Basic " prefix (for a username:password pair) // + // or with a "Bearer " prefix (for a token that can be handled the same as a sessionId cookie) // + ///////////////////////////////////////////////////////////////////////////////////////////////// + String basicPrefix = "Basic "; + String bearerPrefix = "Bearer "; + if(authorizationHeaderValue.startsWith(basicPrefix)) + { + authorizationHeaderValue = authorizationHeaderValue.replaceFirst(basicPrefix, ""); + authenticationContext.put(BASIC_AUTH_NAME, authorizationHeaderValue); + needToSetSessionIdCookie = true; + } + else if(authorizationHeaderValue.startsWith(bearerPrefix)) + { + authorizationHeaderValue = authorizationHeaderValue.replaceFirst(bearerPrefix, ""); + authenticationContext.put(SESSION_ID_COOKIE_NAME, authorizationHeaderValue); + } + else + { + LOG.debug("Authorization header value did not have Basic or Bearer prefix. [" + authorizationHeaderValue + "]"); + } } else { - String authorizationHeaderValue = context.header("Authorization"); - if(authorizationHeaderValue != null) - { - String bearerPrefix = "Bearer "; - if(authorizationHeaderValue.startsWith(bearerPrefix)) - { - authorizationHeaderValue = authorizationHeaderValue.replaceFirst(bearerPrefix, ""); - } - authenticationContext.put(SESSION_ID_COOKIE_NAME, authorizationHeaderValue); - } + LOG.debug("Neither [" + SESSION_ID_COOKIE_NAME + "] cookie nor [Authorization] header was present in request."); } QSession session = authenticationModule.createSession(qInstance, authenticationContext); @@ -363,7 +383,7 @@ public class QJavalinImplementation ///////////////////////////////////////////////////////////////////////////////// // if we got a session id cookie in, then send it back with updated cookie age // ///////////////////////////////////////////////////////////////////////////////// - if(StringUtils.hasContent(sessionIdCookieValue)) + if(needToSetSessionIdCookie) { context.cookie(SESSION_ID_COOKIE_NAME, session.getIdReference(), SESSION_COOKIE_AGE); } @@ -375,10 +395,12 @@ public class QJavalinImplementation //////////////////////////////////////////////////////////////////////////////// // if exception caught, clear out the cookie so the frontend will reauthorize // //////////////////////////////////////////////////////////////////////////////// - if(authenticationModule instanceof Auth0AuthenticationModule) + if(needToSetSessionIdCookie) { context.removeCookie(SESSION_ID_COOKIE_NAME); } + + throw (qae); } } diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationAuthenticationTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationAuthenticationTest.java new file mode 100644 index 00000000..bbfee5dc --- /dev/null +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationAuthenticationTest.java @@ -0,0 +1,224 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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 . + */ + +package com.kingsrook.qqq.backend.javalin; + + +import java.time.ZonedDateTime; +import java.util.Base64; +import java.util.List; +import java.util.concurrent.TimeUnit; +import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; +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.metadata.authentication.TableBasedAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.TableBasedAuthenticationModule; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; +import kong.unirest.Cookie; +import kong.unirest.Cookies; +import kong.unirest.HttpResponse; +import kong.unirest.Unirest; +import org.json.JSONObject; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + + +/******************************************************************************* + ** Tests of QJavalinImplementation, but specifically, of the authentication + ** code - which uses a different qInstance, and hence javalin server instance + ** than the other tests in this package - hence its own before/after, etc. + *******************************************************************************/ +public class QJavalinImplementationAuthenticationTest extends QJavalinTestBase +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + public void beforeEach() throws QInstanceValidationException + { + Unirest.config().reset().enableCookieManagement(false); + setupTableBasedAuthenticationInstance(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @AfterAll + public static void afterAll() + { + if(qJavalinImplementation != null) + { + qJavalinImplementation.stopJavalinServer(); + } + Unirest.config().reset(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAuthentication_noCredentialsProvided() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData").asString(); + assertEquals(401, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + assertEquals("Session ID was not provided", jsonObject.getString("error")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAuthentication_basicAuthSuccess() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData") + .header("Authorization", "Basic " + encodeBasicAuth("juser", "987zyx")) + .asString(); + assertEquals(200, response.getStatus()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAuthentication_basicAuthBadCredentials() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData") + .header("Authorization", "Basic " + encodeBasicAuth("not-juser", "987zyx")) + .asString(); + assertEquals(401, response.getStatus()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAuthentication_authorizationNotBasic() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData") + .header("Authorization", "not-Basic " + encodeBasicAuth("juser", "987zyx")) + .asString(); + assertEquals(401, response.getStatus()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAuthentication_basicAuthSuccessThenSessionIdFromCookie() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData") + .header("Authorization", "Basic " + encodeBasicAuth("juser", "987zyx")) + .asString(); + assertEquals(200, response.getStatus()); + + Cookies cookies = response.getCookies(); + String sessionId = cookies.getNamed("sessionId").getValue(); + ZonedDateTime originalExpiration = cookies.getNamed("sessionId").getExpiration(); + assertNotNull(sessionId); + + SleepUtils.sleep(1, TimeUnit.SECONDS); + + response = Unirest.get(BASE_URL + "/metaData") + .cookie(new Cookie("sessionId", sessionId)) + .asString(); + assertEquals(200, response.getStatus()); + assertEquals(sessionId, response.getCookies().getNamed("sessionId").getValue()); + assertNotEquals(originalExpiration, response.getCookies().getNamed("sessionId").getExpiration()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testAuthentication_badSessionIdCookie() + { + HttpResponse response = Unirest.get(BASE_URL + "/metaData") + .cookie(new Cookie("sessionId", "not-a-sessionId")) + .asString(); + assertEquals(401, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertEquals("Session not found.", jsonObject.getString("error")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + static void setupTableBasedAuthenticationInstance() throws QInstanceValidationException + { + QInstance qInstance = TestUtils.defineInstance(); + TableBasedAuthenticationMetaData tableBasedAuthenticationMetaData = new TableBasedAuthenticationMetaData(); + qInstance.addTable(tableBasedAuthenticationMetaData.defineStandardUserTable(TestUtils.BACKEND_NAME_MEMORY)); + qInstance.addTable(tableBasedAuthenticationMetaData.defineStandardSessionTable(TestUtils.BACKEND_NAME_MEMORY)); + + try + { + TestUtils.insertRecords(qInstance, qInstance.getTable("user"), List.of(new QRecord() + .withValue("username", "juser") + .withValue("fullName", "Johnny User") + .withValue("passwordHash", TableBasedAuthenticationModule.PasswordHasher.createHashedPassword("987zyx")))); + } + catch(Exception e) + { + fail("Error inserting test user."); + } + + qInstance.setAuthentication(tableBasedAuthenticationMetaData); + + restartServerWithInstance(qInstance); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String encodeBasicAuth(String username, String password) + { + Base64.Encoder encoder = Base64.getEncoder(); + String originalString = username + ":" + password; + return (encoder.encodeToString(originalString.getBytes())); + } +} diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java index 571013fa..2eb91ff1 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java @@ -23,7 +23,10 @@ package com.kingsrook.qqq.backend.javalin; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -36,7 +39,19 @@ public class QJavalinTestBase private static final int PORT = 6262; protected static final String BASE_URL = "http://localhost:" + PORT; - private static QJavalinImplementation qJavalinImplementation; + protected static QJavalinImplementation qJavalinImplementation; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @BeforeEach + @AfterEach + void beforeAndAfterEach() + { + MemoryRecordStore.fullReset(); + } @@ -76,4 +91,20 @@ public class QJavalinTestBase TestUtils.primeTestDatabase(); } + + + /******************************************************************************* + ** + *******************************************************************************/ + static protected void restartServerWithInstance(QInstance qInstance) throws QInstanceValidationException + { + if(qJavalinImplementation != null) + { + qJavalinImplementation.stopJavalinServer(); + } + qJavalinImplementation = new QJavalinImplementation(qInstance); + QJavalinProcessHandler.setAsyncStepTimeoutMillis(250); + qJavalinImplementation.startJavalinServer(PORT); + } + } diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java index 3d9d44e6..8a2eeb89 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java @@ -26,16 +26,20 @@ import java.io.InputStream; import java.sql.Connection; import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +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.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +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.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; @@ -59,7 +63,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType; import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; @@ -74,6 +78,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; *******************************************************************************/ public class TestUtils { + public static final String BACKEND_NAME_MEMORY = "memory"; + public static final String TABLE_NAME_PERSON = "person"; public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive"; @@ -209,7 +215,7 @@ public class TestUtils { return new QBackendMetaData() .withBackendType("memory") - .withName("memory"); + .withName(BACKEND_NAME_MEMORY); } @@ -504,4 +510,18 @@ public class TestUtils ); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void insertRecords(QInstance qInstance, QTableMetaData table, List records) throws QException + { + InsertInput insertInput = new InsertInput(qInstance); + insertInput.setSession(new QSession()); + insertInput.setTableName(table.getName()); + insertInput.setRecords(records); + new InsertAction().execute(insertInput); + } + } diff --git a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java index ae0eb6bd..deebfbad 100644 --- a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -82,9 +82,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; 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.Auth0AuthenticationModule; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import io.github.cdimascio.dotenv.Dotenv; @@ -676,7 +676,7 @@ public class QPicoCliImplementation { QQueryFilter filter = new QQueryFilter(); - String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {}); + String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] { }); for(String criterion : criteria) { // todo - parse! @@ -803,7 +803,7 @@ public class QPicoCliImplementation boolean anyFields = false; String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", ""); - String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {}); + String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] { }); if(StringUtils.hasContent(primaryKeyOption)) { @@ -896,7 +896,7 @@ public class QPicoCliImplementation // get the pKeys that the user specified // ///////////////////////////////////////////// String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", ""); - String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {}); + String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] { }); if(StringUtils.hasContent(primaryKeyOption)) { diff --git a/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java b/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java index fb2aced0..13998c96 100644 --- a/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java +++ b/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java @@ -27,7 +27,7 @@ import java.sql.Connection; import java.util.List; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +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.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java index 1436b819..297488ee 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java @@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInpu import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; 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.code.QCodeType; @@ -60,7 +61,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaDa import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;