diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index d48a6a7e..b6e723f1 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -42,6 +42,7 @@ import com.kingsrook.qqq.api.model.APILog; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; 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.tables.ApiTableMetaData; 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.tables.QTableMetaData; 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.QAuthenticationModuleInterface; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -121,6 +123,8 @@ public class QJavalinApiHandler private static Map> tableApiNameMap = new HashMap<>(); + private static Map apiLogUserIdCache = new HashMap<>(); + /******************************************************************************* @@ -366,6 +370,8 @@ public class QJavalinApiHandler } 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 // /////////////////////////////////////////////////////////////////////////// @@ -652,10 +658,27 @@ public class QJavalinApiHandler { if(QContext.getQInstance().getTable(APILog.TABLE_NAME) != null) { + QSession qSession = QContext.getQSession(); + if(qSession != null) + { + for(Map.Entry> 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.setTableName(APILog.TABLE_NAME); - // todo - security fields!!!!! - // todo - user!!!! insertInput.setRecords(List.of(apiLog.toQRecord())); 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> 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); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java index af5c6a4f..02293235 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java @@ -22,15 +22,22 @@ package com.kingsrook.qqq.api.model; +import java.io.Serializable; 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.QRuntimeException; 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.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 { @@ -42,6 +49,9 @@ public class APILog extends QRecordEntity @QField(isEditable = false) private Instant timestamp; + @QField(possibleValueSourceName = APILogMetaDataProvider.TABLE_NAME_API_LOG_USER, label = "User") + private Integer apiLogUserId; + @QField() private String method; @@ -63,6 +73,8 @@ public class APILog extends QRecordEntity @QField() private String responseBody; + private Map 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 entry : CollectionUtils.nonNullMap(this.securityKeyValues).entrySet()) + { + qRecord.setValue(entry.getKey(), entry.getValue()); + } + + return (qRecord); + } + + + /******************************************************************************* ** Constructor ** @@ -390,4 +420,81 @@ public class APILog extends QRecordEntity return (this); } + + + /******************************************************************************* + ** Getter for securityKeyValues + *******************************************************************************/ + public Map getSecurityKeyValues() + { + return (this.securityKeyValues); + } + + + + /******************************************************************************* + ** Setter for securityKeyValues + *******************************************************************************/ + public void setSecurityKeyValues(Map securityKeyValues) + { + this.securityKeyValues = securityKeyValues; + } + + + + /******************************************************************************* + ** Fluent setter for securityKeyValues + *******************************************************************************/ + public APILog withSecurityKeyValues(Map 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); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java index ef04deaf..a08c4441 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java @@ -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.fields.AdornmentType; 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.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.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.model.metadata.tables.UniqueKey; /******************************************************************************* @@ -42,13 +46,63 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; *******************************************************************************/ 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 backendDetailEnricher) throws QException { + defineApiLogUserPvs(qInstance); 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 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 backendDetailEnricher) throws QException { QTableMetaData tableMetaData = new QTableMetaData() - .withName("apiLog") + .withName(TABLE_NAME_API_LOG) .withLabel("API Log") .withIcon(new QIcon().withName("data_object")) .withBackendName(backendName) .withRecordLabelFormat("%s") .withPrimaryKeyField("id") .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("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")))