async def update_global(request: Request, updated_global: GlobalVariable, global_var: UUID, global_col: AsyncIOMotorCollection = Depends(get_mongo_c)): """ Updates a specific Global Variable (fetched by id) and returns it. """ walkoff_db = get_mongo_d(request) curr_user_id = await get_jwt_identity(request) old_global = await mongo_helpers.get_item(global_col, GlobalVariable, global_var) if not old_global: raise DoesNotExistException("update", "Global Variable", global_var) global_id = old_global.id_ new_permissions = updated_global.permissions access_level = new_permissions.access_level to_update = await auth_check(old_global, curr_user_id, "update", walkoff_db) if to_update: if access_level == AccessLevel.CREATOR_ONLY: updated_global.permissions = await creator_only_permissions(curr_user_id) elif access_level == AccessLevel.EVERYONE: updated_global.permissions = await default_permissions(curr_user_id, walkoff_db, "global_variables") elif access_level == AccessLevel.ROLE_BASED: await append_super_and_internal(updated_global.permissions) updated_global.permissions.creator = curr_user_id # try: key = config.get_from_file(config.ENCRYPTION_KEY_PATH, mode='rb') updated_global.value = fernet_encrypt(key, updated_global.value) return await mongo_helpers.update_item(global_col, GlobalVariable, global_id, updated_global) # except Exception as e: # logger.info(e) # raise UniquenessException("global_variable", "update", updated_global.name) else: raise UnauthorizedException("update data for", "Global Variable", old_global.name)
async def read_all_globals(request: Request, to_decrypt: str = False, global_col: AsyncIOMotorCollection = Depends(get_mongo_c), page: int = 1): """ Returns a list of all Global Variables currently loaded in WALKOFF. Pagination is currently not supported. """ walkoff_db = get_mongo_d(request) curr_user_id = await get_jwt_identity(request) # Pagination is currently not supported. if page > 1: return [] key = config.get_from_file(config.ENCRYPTION_KEY_PATH, mode='rb') query = await mongo_helpers.get_all_items(global_col, GlobalVariable) ret = [] if to_decrypt == "false": return query else: for global_var in query: to_read = await auth_check(global_var, curr_user_id, "read", walkoff_db) if to_read: temp_var = deepcopy(global_var) temp_var.value = fernet_decrypt(key, global_var.value) ret.append(temp_var) return ret
async def read_global( request: Request, global_var: UUID, to_decrypt: str = "false", global_col: AsyncIOMotorCollection = Depends(get_mongo_c)): """ Returns the Global Variable for the specified id. """ walkoff_db = get_mongo_d(request) curr_user_id = await get_jwt_identity(request) global_variable = await mongo_helpers.get_item(global_col, GlobalVariable, global_var) to_read = await auth_check(global_variable, curr_user_id, "read", walkoff_db) if to_read: if to_decrypt == "false": return global_variable.value else: key = config.get_from_file(config.ENCRYPTION_KEY_PATH) #, 'rb') # for testing try: key = key.encode('utf-8') key = base64.b64encode(key) except: key = key return fernet_decrypt(key, global_variable.value) else: raise UnauthorizedException("read data for", "Global Variable", global_variable.name)
async def read_all_globals( request: Request, to_decrypt: str = False, global_col: AsyncIOMotorCollection = Depends(get_mongo_c)): """ Returns a list of all Global Variables currently loaded in WALKOFF. """ walkoff_db = get_mongo_d(request) curr_user_id = await get_jwt_identity(request) key = config.get_from_file(config.ENCRYPTION_KEY_PATH) #, 'rb') # for testing try: key = key.encode('utf-8') key = base64.b64encode(key) except: key = key query = await mongo_helpers.get_all_items(global_col, GlobalVariable) ret = [] if to_decrypt == "false": return query else: for global_var in query: to_read = await auth_check(global_var, curr_user_id, "read", walkoff_db) if to_read: temp_var = deepcopy(global_var) temp_var.value = fernet_decrypt(key, global_var.value) ret.append(temp_var) return ret
async def create_global( request: Request, new_global: GlobalVariable, global_col: AsyncIOMotorCollection = Depends(get_mongo_c)): """ Creates a new Global Variable in WALKOFF and returns it. """ walkoff_db = get_mongo_d(request) curr_user_id = await get_jwt_identity(request) permissions = new_global.permissions access_level = permissions.access_level if access_level == AccessLevel.CREATOR_ONLY: new_global.permissions = await creator_only_permissions(curr_user_id) elif access_level == AccessLevel.EVERYONE: new_global.permissions = await default_permissions( curr_user_id, walkoff_db, "global_variables") elif access_level == AccessLevel.ROLE_BASED: await append_super_and_internal(new_global.permissions) new_global.permissions.creator = curr_user_id try: key = config.get_from_file(config.ENCRYPTION_KEY_PATH) #, 'rb') # for testing try: key = key.encode('utf-8') key = base64.b64encode(key) except: key = key new_global.value = fernet_encrypt(key, new_global.value) return await mongo_helpers.create_item(global_col, GlobalVariable, new_global) except Exception as e: logger.info(e) raise UniquenessException("global_variable", "create", new_global.name)
async def delete_global(request: Request, global_var: UUID, global_col: AsyncIOMotorCollection = Depends(get_mongo_c)): """ Deletes a specific Global Variable (fetched by id). """ walkoff_db = get_mongo_d(request) curr_user_id = await get_jwt_identity(request) global_variable = await mongo_helpers.get_item(global_col, GlobalVariable, global_var) if not global_variable: raise DoesNotExistException("delete", "Global Variable", global_var) global_id = global_variable.id_ to_delete = await auth_check(global_variable, curr_user_id, "delete", walkoff_db) if to_delete: return await mongo_helpers.delete_item(global_col, GlobalVariable, global_id) else: raise UnauthorizedException("delete data for", "Global Variable", global_variable.name)
async def control_workflow( request: Request, execution, workflow_to_control: ControlWorkflow, workflow_status_col: AsyncIOMotorCollection = Depends(get_mongo_c)): """ Pause, resume, or abort a workflow currently executing in WALKOFF. """ execution = await mongo_helpers.get_item(workflow_status_col, WorkflowStatus, execution, id_key="execution_id") walkoff_db = get_mongo_d(request) workflow_col = walkoff_db.workflows curr_user_id = await get_jwt_identity(request) workflow_id = execution.workflow_id data = dict(workflow_to_control) status = data['status'] workflow: WorkflowModel = await mongo_helpers.get_item( workflow_col, WorkflowModel, workflow_id) # workflow = workflow_getter(execution.workflow_id, workflow_status_col) # The resource factory returns the WorkflowStatus model but we want the string of the execution ID execution_id = str(execution.execution_id) to_execute = await auth_check(workflow, curr_user_id, "execute", walkoff_db=walkoff_db) # TODO: add in pause/resume here. Workers need to store and recover state for this if to_execute: if status.lower() == 'abort': logger.info( f"User '{(await get_jwt_claims(request)).get('username', None)}' aborting workflow: {execution_id}" ) message = { "execution_id": execution_id, "status": status, "workflow": dict(workflow) } async with connect_to_aioredis_pool(config.REDIS_URI) as conn: await conn.smove(static.REDIS_PENDING_WORKFLOWS, static.REDIS_ABORTING_WORKFLOWS, execution_id) await conn.xadd(static.REDIS_WORKFLOW_CONTROL, message) return None, HTTPStatus.NO_CONTENT elif status.lower() == 'trigger': if execution.status not in (StatusEnum.PENDING, StatusEnum.EXECUTING, StatusEnum.AWAITING_DATA): raise InvalidInputException( "workflow", "trigger", execution_id, errors=[ "Workflow must be in a running state to accept triggers." ]) trigger_id = data.get('trigger_id') if not trigger_id: raise InvalidInputException( "workflow", "trigger", execution_id, errors=[ "ID of the trigger must be specified in trigger_id." ]) seen = False for trigger in workflow.triggers: if str(trigger.id_) == trigger_id: seen = True if not seen: raise InvalidInputException( "workflow", "trigger", execution_id, errors=[ f"trigger_id {trigger_id} was not found in this workflow." ]) trigger_stream = f"{execution_id}-{trigger_id}:triggers" try: async with connect_to_aioredis_pool(config.REDIS_URI) as conn: info = await conn.xinfo_stream(trigger_stream) stream_length = info["length"] except Exception: stream_length = 0 if stream_length > 0: return InvalidInputException( "workflow", "trigger", execution_id, errors=[f"This trigger has already received data."]) trigger_data = data.get('trigger_data') logger.info( f"User '{(await get_jwt_claims(request)).get('username', None)}' triggering workflow: {execution_id} at trigger " f"{trigger_id} with data {trigger_data}") async with connect_to_aioredis_pool(config.REDIS_URI) as conn: await conn.xadd(trigger_stream, { execution_id: message_dumps({"trigger_data": trigger_data}) }) return ({"trigger_stream": trigger_stream}) else: return None
async def execute_workflow( workflow_to_execute: ExecuteWorkflow, request: Request, workflow_status_col: AsyncIOMotorCollection = Depends(get_mongo_c)): """ Executes a WALKOFF workflow. """ walkoff_db = get_mongo_d(request) workflow_col = walkoff_db.workflows workflow_id = workflow_to_execute.workflow_id execution_id = workflow_to_execute.execution_id workflow: WorkflowModel = await mongo_helpers.get_item( workflow_col, WorkflowModel, workflow_id) # workflow = workflow_getter(workflow_id, workflow_status_col) # data = dict(workflow_to_execute) curr_user_id = await get_jwt_identity(request) to_execute = await auth_check(workflow, curr_user_id, "execute", walkoff_db=walkoff_db) if to_execute: if not workflow: raise DoesNotExistException("workflow", "execute", workflow_id) if not workflow.is_valid: raise InvalidInputException("workflow", "execute", workflow.id_, errors=workflow.errors) # workflow = dict(workflow) actions_by_id = {a.id_: a for a in workflow.actions} triggers_by_id = {t.id_: t for t in workflow.triggers} # TODO: Add validation to all overrides if "start" in workflow_to_execute: if workflow_to_execute.start in actions_by_id or workflow_to_execute.start in triggers_by_id: workflow.start = workflow_to_execute.start else: raise InvalidInputException( "execute", "workflow", workflow.id_, errors=[ "Start override must be an action or a trigger in this workflow." ]) if "workflow_variables" in workflow and "workflow_variables" in workflow_to_execute: # TODO: change these on the db model to be keyed by ID # Get workflow variables keyed by ID current_wvs = {wv.id_: wv for wv in workflow.workflow_variables} new_wvs = { wv['id_']: wv for wv in workflow_to_execute.workflow_variables } # Update workflow variables with new values, ignore ids that didn't already exist override_wvs = { id_: new_wvs[id_] if id_ in new_wvs else current_wvs[id_] for id_ in current_wvs } workflow.workflow_variables = list(override_wvs.values()) if "parameters" in workflow_to_execute: if workflow_to_execute.start is not None: start_id = workflow_to_execute.start else: start_id = workflow.start # start_id = data.get("start", workflow.start) if start_id in actions_by_id: parameters_by_name = { p.name: p for p in actions_by_id[start_id].parameters } for parameter in workflow_to_execute.parameters: parameters_by_name[parameter.name] = parameter actions_by_id[start_id].parameters = list( parameters_by_name.values()) workflow.actions = list(actions_by_id.values()) else: raise InvalidInputException( "workflow", "execute", workflow.id_, errors=[ "Cannot override starting parameters for anything but an action." ]) try: # TODO: add check for workflow_col VALIDATION execution_id = await execute_workflow_helper( request=request, workflow_id=workflow_id, workflow_col=workflow_col, execution_id=execution_id, workflow=workflow, workflow_status_col=workflow_status_col) return {'execution_id': execution_id} except ValidationError as e: raise ImproperJSONException('workflow_status', 'create', workflow.name, e) # raise e.messages else: raise HTTPException(status_code=403, detail="Forbidden")