|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
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<String, Integer> 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<String, Integer> 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|