Add api logs user, security fields

This commit is contained in:
2023-04-03 10:35:11 -05:00
parent b021aebabb
commit ffe8da448b
3 changed files with 312 additions and 5 deletions

View File

@ -42,6 +42,7 @@ import com.kingsrook.qqq.api.model.APILog;
import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.APIVersion;
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput;
import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput;
import com.kingsrook.qqq.api.model.metadata.APILogMetaDataProvider;
import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData; import com.kingsrook.qqq.api.model.metadata.ApiInstanceMetaData;
import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData; import com.kingsrook.qqq.api.model.metadata.tables.ApiTableMetaData;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
@ -85,6 +86,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; 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.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession; 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.QAuthenticationModuleDispatcher; 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.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -121,6 +123,8 @@ public class QJavalinApiHandler
private static Map<String, Map<String, QTableMetaData>> tableApiNameMap = new HashMap<>(); private static Map<String, Map<String, QTableMetaData>> tableApiNameMap = new HashMap<>();
private static Map<String, Integer> apiLogUserIdCache = new HashMap<>();
/******************************************************************************* /*******************************************************************************
@ -366,6 +370,8 @@ public class QJavalinApiHandler
} }
catch(AccessTokenException aae) catch(AccessTokenException aae)
{ {
LOG.info("Error getting api access token", aae, logPair("clientId", clientId));
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// if the exception has a status code, then return that code and message // // if the exception has a status code, then return that code and message //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -652,10 +658,27 @@ public class QJavalinApiHandler
{ {
if(QContext.getQInstance().getTable(APILog.TABLE_NAME) != null) if(QContext.getQInstance().getTable(APILog.TABLE_NAME) != null)
{ {
QSession qSession = QContext.getQSession();
if(qSession != null)
{
for(Map.Entry<String, List<Serializable>> entry : CollectionUtils.nonNullMap(qSession.getSecurityKeyValues()).entrySet())
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// put the 1st entry for this key in the api log record //
// todo - might need revisited for users with multiple values... e.g., look for the security key in records in the request? or as part of the URL //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(entry.getValue()))
{
apiLog.withSecurityKeyValue(entry.getKey(), entry.getValue().get(0));
}
}
Integer userId = getApiLogUserId(qSession);
apiLog.setApiLogUserId(userId);
}
InsertInput insertInput = new InsertInput(); InsertInput insertInput = new InsertInput();
insertInput.setTableName(APILog.TABLE_NAME); insertInput.setTableName(APILog.TABLE_NAME);
// todo - security fields!!!!!
// todo - user!!!!
insertInput.setRecords(List.of(apiLog.toQRecord())); insertInput.setRecords(List.of(apiLog.toQRecord()));
new InsertAction().executeAsync(insertInput); new InsertAction().executeAsync(insertInput);
} }
@ -668,6 +691,129 @@ public class QJavalinApiHandler
/*******************************************************************************
**
*******************************************************************************/
private static Integer getApiLogUserId(QSession qSession) throws QException
{
String tableName = APILogMetaDataProvider.TABLE_NAME_API_LOG_USER;
if(qSession == null)
{
return (null);
}
QUser qUser = qSession.getUser();
if(qUser == null)
{
return (null);
}
String userName = qUser.getFullName();
if(!StringUtils.hasContent(userName))
{
return (null);
}
/////////////////////////////////////////////////////////////////////////////
// if we haven't cached this username to an id, query and/or insert it now //
/////////////////////////////////////////////////////////////////////////////
if(!apiLogUserIdCache.containsKey(userName))
{
//////////////////////////////////////////////////////////////
// first try to get - if it's found, cache it and return it //
//////////////////////////////////////////////////////////////
Integer id = fetchApiLogUserIdFromName(userName);
if(id != null)
{
apiLogUserIdCache.put(userName, id);
return id;
}
try
{
///////////////////////////////////////////////////////
// if it wasn't found from a Get, then try an Insert //
///////////////////////////////////////////////////////
LOG.debug("Inserting " + tableName + " named " + userName);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(tableName);
QRecord record = new QRecord().withValue("name", userName);
for(Map.Entry<String, List<Serializable>> entry : CollectionUtils.nonNullMap(qSession.getSecurityKeyValues()).entrySet())
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// put the 1st entry for this key in the api log user record //
// todo - might need revisited for users with multiple values... e.g., look for the security key in records in the request? or as part of the URL //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(entry.getValue()))
{
record.withValue(entry.getKey(), entry.getValue().get(0));
}
}
insertInput.setRecords(List.of(record));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
id = insertOutput.getRecords().get(0).getValueInteger("id");
////////////////////////////////////////
// if we got an id, cache & return it //
////////////////////////////////////////
if(id != null)
{
apiLogUserIdCache.put(userName, id);
return id;
}
}
catch(Exception e)
{
////////////////////////////////////////////////////////////////////
// assume this may mean a dupe-key - so - try another fetch below //
////////////////////////////////////////////////////////////////////
LOG.debug("Caught error inserting " + tableName + " named " + userName + " - will try to re-fetch", e);
}
//////////////////////////////////////////////////////////////////////////
// if the insert failed, try another fetch (e.g., after a UK violation) //
//////////////////////////////////////////////////////////////////////////
id = fetchApiLogUserIdFromName(userName);
if(id != null)
{
apiLogUserIdCache.put(userName, id);
return id;
}
/////////////
// give up //
/////////////
LOG.error("Unable to get id for " + tableName + " named " + userName);
return (null);
}
return (apiLogUserIdCache.get(userName));
}
/*******************************************************************************
**
*******************************************************************************/
private static Integer fetchApiLogUserIdFromName(String name) throws QException
{
GetInput getInput = new GetInput();
getInput.setTableName(APILogMetaDataProvider.TABLE_NAME_API_LOG_USER);
getInput.setUniqueKey(Map.of("name", name));
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() != null)
{
return (getOutput.getRecord().getValueInteger("id"));
}
return (null);
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -22,15 +22,22 @@
package com.kingsrook.qqq.api.model; package com.kingsrook.qqq.api.model;
import java.io.Serializable;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.api.model.metadata.APILogMetaDataProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.model.data.QField; import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/******************************************************************************* /*******************************************************************************
** ** In addition to the standard/known fields in this entity, you can also add
** name/value pairs of security key values - e.g., a clientId field
*******************************************************************************/ *******************************************************************************/
public class APILog extends QRecordEntity public class APILog extends QRecordEntity
{ {
@ -42,6 +49,9 @@ public class APILog extends QRecordEntity
@QField(isEditable = false) @QField(isEditable = false)
private Instant timestamp; private Instant timestamp;
@QField(possibleValueSourceName = APILogMetaDataProvider.TABLE_NAME_API_LOG_USER, label = "User")
private Integer apiLogUserId;
@QField() @QField()
private String method; private String method;
@ -63,6 +73,8 @@ public class APILog extends QRecordEntity
@QField() @QField()
private String responseBody; private String responseBody;
private Map<String, Serializable> securityKeyValues = new HashMap<>();
/******************************************************************************* /*******************************************************************************
@ -75,6 +87,24 @@ public class APILog extends QRecordEntity
/*******************************************************************************
**
*******************************************************************************/
@Override
public QRecord toQRecord() throws QRuntimeException
{
QRecord qRecord = super.toQRecord();
for(Map.Entry<String, Serializable> entry : CollectionUtils.nonNullMap(this.securityKeyValues).entrySet())
{
qRecord.setValue(entry.getKey(), entry.getValue());
}
return (qRecord);
}
/******************************************************************************* /*******************************************************************************
** Constructor ** Constructor
** **
@ -390,4 +420,81 @@ public class APILog extends QRecordEntity
return (this); return (this);
} }
/*******************************************************************************
** Getter for securityKeyValues
*******************************************************************************/
public Map<String, Serializable> getSecurityKeyValues()
{
return (this.securityKeyValues);
}
/*******************************************************************************
** Setter for securityKeyValues
*******************************************************************************/
public void setSecurityKeyValues(Map<String, Serializable> securityKeyValues)
{
this.securityKeyValues = securityKeyValues;
}
/*******************************************************************************
** Fluent setter for securityKeyValues
*******************************************************************************/
public APILog withSecurityKeyValues(Map<String, Serializable> securityKeyValues)
{
this.securityKeyValues = securityKeyValues;
return (this);
}
/*******************************************************************************
** Fluent setter for securityKeyValues
*******************************************************************************/
public APILog withSecurityKeyValue(String key, Serializable value)
{
if(this.securityKeyValues == null)
{
this.securityKeyValues = new HashMap<>();
}
this.securityKeyValues.put(key, value);
return (this);
}
/*******************************************************************************
** Getter for apiLogUserId
*******************************************************************************/
public Integer getApiLogUserId()
{
return (this.apiLogUserId);
}
/*******************************************************************************
** Setter for apiLogUserId
*******************************************************************************/
public void setApiLogUserId(Integer apiLogUserId)
{
this.apiLogUserId = apiLogUserId;
}
/*******************************************************************************
** Fluent setter for apiLogUserId
*******************************************************************************/
public APILog withApiLogUserId(Integer apiLogUserId)
{
this.apiLogUserId = apiLogUserId;
return (this);
}
} }

View File

@ -29,12 +29,16 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment; import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
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.fields.ValueTooLongBehavior; import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; 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.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
/******************************************************************************* /*******************************************************************************
@ -42,13 +46,63 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
*******************************************************************************/ *******************************************************************************/
public class APILogMetaDataProvider public class APILogMetaDataProvider
{ {
public static final String TABLE_NAME_API_LOG = "apiLog";
public static final String TABLE_NAME_API_LOG_USER = "apiLogUser";
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
public static void defineAll(QInstance qInstance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException public static void defineAll(QInstance qInstance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{ {
defineApiLogUserPvs(qInstance);
defineAPILogTable(qInstance, backendName, backendDetailEnricher); defineAPILogTable(qInstance, backendName, backendDetailEnricher);
defineAPILogUserTable(qInstance, backendName, backendDetailEnricher);
}
/*******************************************************************************
**
*******************************************************************************/
private static void defineApiLogUserPvs(QInstance instance)
{
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(TABLE_NAME_API_LOG_USER)
.withTableName(TABLE_NAME_API_LOG_USER));
}
/*******************************************************************************
**
*******************************************************************************/
private static void defineAPILogUserTable(QInstance qInstance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
QTableMetaData tableMetaData = new QTableMetaData()
.withName(TABLE_NAME_API_LOG_USER)
.withLabel("API Log User")
.withIcon(new QIcon().withName("person"))
.withBackendName(backendName)
.withRecordLabelFormat("%s")
.withRecordLabelFields("name")
.withPrimaryKeyField("id")
.withUniqueKey(new UniqueKey("name"))
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
.withField(new QFieldMetaData("name", QFieldType.STRING).withIsRequired(true))
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "name")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")))
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
if(backendDetailEnricher != null)
{
backendDetailEnricher.accept(tableMetaData);
}
qInstance.addTable(tableMetaData);
} }
@ -59,14 +113,14 @@ public class APILogMetaDataProvider
private static void defineAPILogTable(QInstance qInstance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException private static void defineAPILogTable(QInstance qInstance, String backendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{ {
QTableMetaData tableMetaData = new QTableMetaData() QTableMetaData tableMetaData = new QTableMetaData()
.withName("apiLog") .withName(TABLE_NAME_API_LOG)
.withLabel("API Log") .withLabel("API Log")
.withIcon(new QIcon().withName("data_object")) .withIcon(new QIcon().withName("data_object"))
.withBackendName(backendName) .withBackendName(backendName)
.withRecordLabelFormat("%s") .withRecordLabelFormat("%s")
.withPrimaryKeyField("id") .withPrimaryKeyField("id")
.withFieldsFromEntity(APILog.class) .withFieldsFromEntity(APILog.class)
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id"))) .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "apiLogUserId")))
.withSection(new QFieldSection("request", new QIcon().withName("arrow_upward"), Tier.T2, List.of("method", "version", "path", "queryString", "requestBody"))) .withSection(new QFieldSection("request", new QIcon().withName("arrow_upward"), Tier.T2, List.of("method", "version", "path", "queryString", "requestBody")))
.withSection(new QFieldSection("response", new QIcon().withName("arrow_downward"), Tier.T2, List.of("statusCode", "responseBody"))) .withSection(new QFieldSection("response", new QIcon().withName("arrow_downward"), Tier.T2, List.of("statusCode", "responseBody")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("timestamp"))) .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("timestamp")))