implementation of record security locks, and permissions

This commit is contained in:
2023-01-11 10:24:31 -06:00
parent e4d37e3db9
commit 23e9abeb74
83 changed files with 6639 additions and 504 deletions

View File

@ -41,6 +41,9 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetAction;
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction;
import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
import com.kingsrook.qqq.backend.core.actions.reporting.ExportAction;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
@ -54,6 +57,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
@ -86,6 +90,7 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
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.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.QAuthenticationModuleDispatcher;
@ -331,7 +336,6 @@ public class QJavalinImplementation
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData());
boolean needToSetSessionIdCookie = false;
try
{
Map<String, String> authenticationContext = new HashMap<>();
@ -345,7 +349,6 @@ public class QJavalinImplementation
// first, look for a sessionId cookie //
////////////////////////////////////////
authenticationContext.put(SESSION_ID_COOKIE_NAME, sessionIdCookieValue);
needToSetSessionIdCookie = true;
}
else if(authorizationHeaderValue != null)
{
@ -360,7 +363,6 @@ public class QJavalinImplementation
{
authorizationHeaderValue = authorizationHeaderValue.replaceFirst(basicPrefix, "");
authenticationContext.put(BASIC_AUTH_NAME, authorizationHeaderValue);
needToSetSessionIdCookie = true;
}
else if(authorizationHeaderValue.startsWith(bearerPrefix))
{
@ -383,7 +385,7 @@ public class QJavalinImplementation
/////////////////////////////////////////////////////////////////////////////////
// if we got a session id cookie in, then send it back with updated cookie age //
/////////////////////////////////////////////////////////////////////////////////
if(needToSetSessionIdCookie)
if(authenticationModule.usesSessionIdCookie())
{
context.cookie(SESSION_ID_COOKIE_NAME, session.getIdReference(), SESSION_COOKIE_AGE);
}
@ -395,7 +397,7 @@ public class QJavalinImplementation
////////////////////////////////////////////////////////////////////////////////
// if exception caught, clear out the cookie so the frontend will reauthorize //
////////////////////////////////////////////////////////////////////////////////
if(needToSetSessionIdCookie)
if(authenticationModule.usesSessionIdCookie())
{
context.removeCookie(SESSION_ID_COOKIE_NAME);
}
@ -446,6 +448,8 @@ public class QJavalinImplementation
deleteInput.setTableName(table);
deleteInput.setPrimaryKeys(primaryKeys);
PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE);
DeleteAction deleteAction = new DeleteAction();
DeleteOutput deleteResult = deleteAction.execute(deleteInput);
@ -466,7 +470,14 @@ public class QJavalinImplementation
{
try
{
String table = context.pathParam("table");
String table = context.pathParam("table");
UpdateInput updateInput = new UpdateInput(qInstance);
setupSession(context, updateInput);
updateInput.setTableName(table);
PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT);
List<QRecord> recordList = new ArrayList<>();
QRecord record = new QRecord();
record.setTableName(table);
@ -494,12 +505,8 @@ public class QJavalinImplementation
}
QTableMetaData tableMetaData = qInstance.getTable(table);
record.setValue(tableMetaData.getPrimaryKeyField(), context.pathParam("primaryKey"));
UpdateInput updateInput = new UpdateInput(qInstance);
setupSession(context, updateInput);
updateInput.setTableName(table);
updateInput.setRecords(recordList);
UpdateAction updateAction = new UpdateAction();
@ -522,7 +529,13 @@ public class QJavalinImplementation
{
try
{
String table = context.pathParam("table");
String table = context.pathParam("table");
InsertInput insertInput = new InsertInput(qInstance);
setupSession(context, insertInput);
insertInput.setTableName(table);
PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT);
List<QRecord> recordList = new ArrayList<>();
QRecord record = new QRecord();
record.setTableName(table);
@ -536,10 +549,6 @@ public class QJavalinImplementation
record.setValue(String.valueOf(entry.getKey()), (Serializable) entry.getValue());
}
}
InsertInput insertInput = new InsertInput(qInstance);
setupSession(context, insertInput);
insertInput.setTableName(table);
insertInput.setRecords(recordList);
InsertAction insertAction = new InsertAction();
@ -577,6 +586,8 @@ public class QJavalinImplementation
getInput.setShouldGenerateDisplayValues(true);
getInput.setShouldTranslatePossibleValues(true);
PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ);
// todo - validate that the primary key is of the proper type (e.g,. not a string for an id field)
// and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error)
@ -624,6 +635,8 @@ public class QJavalinImplementation
setupSession(context, countInput);
countInput.setTableName(context.pathParam("table"));
PermissionsHelper.checkTablePermissionThrowing(countInput, TablePermissionSubType.READ);
String filter = stringQueryParam(context, "filter");
if(!StringUtils.hasContent(filter))
{
@ -673,6 +686,8 @@ public class QJavalinImplementation
queryInput.setSkip(integerQueryParam(context, "skip"));
queryInput.setLimit(integerQueryParam(context, "limit"));
PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ);
String filter = stringQueryParam(context, "filter");
if(!StringUtils.hasContent(filter))
{
@ -727,7 +742,22 @@ public class QJavalinImplementation
{
TableMetaDataInput tableMetaDataInput = new TableMetaDataInput(qInstance);
setupSession(context, tableMetaDataInput);
tableMetaDataInput.setTableName(context.pathParam("table"));
String tableName = context.pathParam("table");
QTableMetaData table = qInstance.getTable(tableName);
if(table == null)
{
throw (new QNotFoundException("Table [" + tableName + "] was not found."));
}
PermissionCheckResult permissionCheckResult = PermissionsHelper.getPermissionCheckResult(tableMetaDataInput, table);
if(permissionCheckResult.equals(PermissionCheckResult.DENY_HIDE))
{
// not found? or permission denied... hmm
throw (new QNotFoundException("Table [" + tableName + "] was not found."));
}
tableMetaDataInput.setTableName(tableName);
TableMetaDataAction tableMetaDataAction = new TableMetaDataAction();
TableMetaDataOutput tableMetaDataOutput = tableMetaDataAction.execute(tableMetaDataInput);
@ -750,7 +780,16 @@ public class QJavalinImplementation
{
ProcessMetaDataInput processMetaDataInput = new ProcessMetaDataInput(qInstance);
setupSession(context, processMetaDataInput);
processMetaDataInput.setProcessName(context.pathParam("processName"));
String processName = context.pathParam("processName");
QProcessMetaData process = qInstance.getProcess(processName);
if(process == null)
{
throw (new QNotFoundException("Process [" + processName + "] was not found."));
}
PermissionsHelper.checkProcessPermissionThrowing(processMetaDataInput, processName);
processMetaDataInput.setProcessName(processName);
ProcessMetaDataAction processMetaDataAction = new ProcessMetaDataAction();
ProcessMetaDataOutput processMetaDataOutput = processMetaDataAction.execute(processMetaDataInput);
@ -778,6 +817,8 @@ public class QJavalinImplementation
.withSession(insertInput.getSession())
.withWidgetMetaData(qInstance.getWidget(context.pathParam("name")));
// todo permission?
//////////////////////////
// process query string //
//////////////////////////
@ -856,6 +897,8 @@ public class QJavalinImplementation
exportInput.setFilename(filename);
exportInput.setLimit(limit);
PermissionsHelper.checkTablePermissionThrowing(exportInput, TablePermissionSubType.READ);
String fields = stringQueryParam(context, "fields");
if(StringUtils.hasContent(fields))
{
@ -1137,6 +1180,12 @@ public class QJavalinImplementation
return;
}
if(e instanceof QPermissionDeniedException)
{
respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage()); // 403
return;
}
////////////////////////////////
// default exception handling //
////////////////////////////////

View File

@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction;
@ -53,7 +54,9 @@ import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
import com.kingsrook.qqq.backend.core.model.actions.processes.QUploadedFile;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
@ -185,6 +188,8 @@ public class QJavalinProcessHandler
/////////////////////////////////////////////
ReportInput reportInput = new ReportInput(QJavalinImplementation.qInstance);
QJavalinImplementation.setupSession(context, reportInput);
PermissionsHelper.checkReportPermissionThrowing(reportInput, reportName);
reportInput.setReportFormat(reportFormat);
reportInput.setReportName(reportName);
reportInput.setInputValues(null); // todo!
@ -293,12 +298,19 @@ public class QJavalinProcessHandler
RunProcessInput runProcessInput = new RunProcessInput(QJavalinImplementation.qInstance);
QJavalinImplementation.setupSession(context, runProcessInput);
runProcessInput.setProcessName(processName);
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK);
runProcessInput.setProcessUUID(processUUID);
runProcessInput.setStartAfterStep(startAfterStep);
populateRunProcessRequestWithValuesFromContext(context, runProcessInput);
//////////////////////////////////////////////////////////////////////////////////////////////////
// important to do this check AFTER the runProcessInput is populated with values from context - //
// e.g., in case things like a reportName are set in here //
//////////////////////////////////////////////////////////////////////////////////////////////////
PermissionsHelper.checkProcessPermissionThrowing(runProcessInput, processName);
////////////////////////////////////////
// run the process as an async action //
////////////////////////////////////////
@ -321,6 +333,10 @@ public class QJavalinProcessHandler
{
resultForCaller.put("jobUUID", jgae.getJobUUID());
}
catch(QPermissionDeniedException pde)
{
QJavalinImplementation.handleException(context, pde);
}
catch(Exception e)
{
//////////////////////////////////////////////////////////////////////////////
@ -549,55 +565,68 @@ public class QJavalinProcessHandler
*******************************************************************************/
public static void processStatus(Context context)
{
String processUUID = context.pathParam("processUUID");
String jobUUID = context.pathParam("jobUUID");
Map<String, Object> resultForCaller = new HashMap<>();
resultForCaller.put("processUUID", processUUID);
LOG.debug("Request for status of process " + processUUID + ", job " + jobUUID);
Optional<AsyncJobStatus> optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID);
if(optionalJobStatus.isEmpty())
try
{
serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find status of process step job"));
}
else
{
AsyncJobStatus jobStatus = optionalJobStatus.get();
AbstractActionInput input = new AbstractActionInput(QJavalinImplementation.qInstance);
QJavalinImplementation.setupSession(context, input);
resultForCaller.put("jobStatus", jobStatus);
LOG.debug("Job status is " + jobStatus.getState() + " for " + jobUUID);
// todo... get process values? PermissionsHelper.checkProcessPermissionThrowing(input, context.pathParam("processName"));
if(jobStatus.getState().equals(AsyncJobState.COMPLETE))
String processUUID = context.pathParam("processUUID");
String jobUUID = context.pathParam("jobUUID");
resultForCaller.put("processUUID", processUUID);
LOG.debug("Request for status of process " + processUUID + ", job " + jobUUID);
Optional<AsyncJobStatus> optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID);
if(optionalJobStatus.isEmpty())
{
///////////////////////////////////////////////////////////////////////////////////////
// if the job is complete, get the process result from state provider, and return it //
// this output should look like it did if the job finished synchronously!! //
///////////////////////////////////////////////////////////////////////////////////////
Optional<ProcessState> processState = RunProcessAction.getState(processUUID);
if(processState.isPresent())
serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find status of process step job"));
}
else
{
AsyncJobStatus jobStatus = optionalJobStatus.get();
resultForCaller.put("jobStatus", jobStatus);
LOG.debug("Job status is " + jobStatus.getState() + " for " + jobUUID);
if(jobStatus.getState().equals(AsyncJobState.COMPLETE))
{
RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get());
serializeRunProcessResultForCaller(resultForCaller, runProcessOutput);
///////////////////////////////////////////////////////////////////////////////////////
// if the job is complete, get the process result from state provider, and return it //
// this output should look like it did if the job finished synchronously!! //
///////////////////////////////////////////////////////////////////////////////////////
Optional<ProcessState> processState = RunProcessAction.getState(processUUID);
if(processState.isPresent())
{
RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get());
serializeRunProcessResultForCaller(resultForCaller, runProcessOutput);
}
else
{
serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find results for process " + processUUID));
}
}
else
else if(jobStatus.getState().equals(AsyncJobState.ERROR))
{
serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find results for process " + processUUID));
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
if(jobStatus.getCaughtException() != null)
{
serializeRunProcessExceptionForCaller(resultForCaller, jobStatus.getCaughtException());
}
}
}
else if(jobStatus.getState().equals(AsyncJobState.ERROR))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
if(jobStatus.getCaughtException() != null)
{
serializeRunProcessExceptionForCaller(resultForCaller, jobStatus.getCaughtException());
}
}
}
context.result(JsonUtils.toJson(resultForCaller));
context.result(JsonUtils.toJson(resultForCaller));
}
catch(Exception e)
{
serializeRunProcessExceptionForCaller(resultForCaller, e);
}
}
@ -609,6 +638,10 @@ public class QJavalinProcessHandler
{
try
{
AbstractActionInput input = new AbstractActionInput(QJavalinImplementation.qInstance);
QJavalinImplementation.setupSession(context, input);
// todo - need process values? PermissionsHelper.checkProcessPermissionThrowing(input, context.pathParam("processName"));
String processUUID = context.pathParam("processUUID");
Integer skip = Objects.requireNonNullElse(QJavalinImplementation.integerQueryParam(context, "skip"), 0);
Integer limit = Objects.requireNonNullElse(QJavalinImplementation.integerQueryParam(context, "limit"), 20);

View File

@ -29,10 +29,13 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
import com.kingsrook.qqq.backend.core.actions.scripts.StoreAssociatedScriptAction;
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput;
@ -75,6 +78,7 @@ public class QJavalinScriptsHandler
*******************************************************************************/
public static void defineRecordRoutes()
{
// todo - do we want some generic "developer mode" permission??
get("/developer", QJavalinScriptsHandler::getRecordDeveloperMode);
post("/developer/associatedScript/{fieldName}", QJavalinScriptsHandler::storeRecordAssociatedScript);
get("/developer/associatedScript/{fieldName}/{scriptRevisionId}/logs", QJavalinScriptsHandler::getAssociatedScriptLogs);
@ -100,6 +104,8 @@ public class QJavalinScriptsHandler
getInput.setShouldGenerateDisplayValues(true);
getInput.setShouldTranslatePossibleValues(true);
PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ);
// todo - validate that the primary key is of the proper type (e.g,. not a string for an id field)
// and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error)
@ -217,6 +223,8 @@ public class QJavalinScriptsHandler
{
try
{
getReferencedRecordToEnsureAccess(context);
String scriptRevisionId = context.pathParam("scriptRevisionId");
QueryInput queryInput = new QueryInput(QJavalinImplementation.qInstance);
@ -247,6 +255,40 @@ public class QJavalinScriptsHandler
/*******************************************************************************
**
*******************************************************************************/
private static void getReferencedRecordToEnsureAccess(Context context) throws QException
{
/////////////////////////////////////////////////////////////////////////////////
// make sure user can get the record they're trying to do a related action for //
/////////////////////////////////////////////////////////////////////////////////
String tableName = context.pathParam("table");
QTableMetaData table = QJavalinImplementation.qInstance.getTable(tableName);
GetInput getInput = new GetInput(QJavalinImplementation.qInstance);
getInput.setTableName(tableName);
QJavalinImplementation.setupSession(context, getInput);
PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ);
String primaryKey = context.pathParam("primaryKey");
getInput.setPrimaryKey(primaryKey);
GetAction getAction = new GetAction();
GetOutput getOutput = getAction.execute(getInput);
///////////////////////////////////////////////////////
// throw a not found error if the record isn't found //
///////////////////////////////////////////////////////
QRecord record = getOutput.getRecord();
if(record == null)
{
throw (new QNotFoundException("Could not find " + table.getLabel() + " with "
+ table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey));
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -258,12 +300,15 @@ public class QJavalinScriptsHandler
{
StoreAssociatedScriptInput input = new StoreAssociatedScriptInput(QJavalinImplementation.qInstance);
QJavalinImplementation.setupSession(context, input);
input.setCode(context.formParam("contents"));
input.setCommitMessage(context.formParam("commitMessage"));
input.setFieldName(context.pathParam("fieldName"));
input.setTableName(context.pathParam("table"));
input.setRecordPrimaryKey(context.pathParam("primaryKey"));
PermissionsHelper.checkTablePermissionThrowing(input, TablePermissionSubType.EDIT); // todo ... is this enough??
StoreAssociatedScriptOutput output = new StoreAssociatedScriptOutput();
StoreAssociatedScriptAction storeAssociatedScriptAction = new StoreAssociatedScriptAction();
@ -288,6 +333,8 @@ public class QJavalinScriptsHandler
try
{
getReferencedRecordToEnsureAccess(context);
TestScriptInput input = new TestScriptInput(QJavalinImplementation.qInstance);
QJavalinImplementation.setupSession(context, input);