def _load_wf_module_and_service( workflow: Workflow, wf_module_id: int, param: str) -> Tuple[WfModule, oauth.OAuthService]: """ Load WfModule and OAuthService from the database, or raise. Raise WfModule.DoesNotExist if the WfModule is deleted or missing. Raise ModuleVersion.DoesNotExist if the WfModule does not have the given param. Invoke this within a Workflow.cooperative_lock(). """ # raises WfModule.DoesNotExist wf_module = WfModule.live_in_workflow(workflow).get(pk=wf_module_id) # raise ModuleVersion.DoesNotExist if ModuleVersion was deleted module_version = wf_module.module_version if module_version is None: raise ModuleVersion.DoesNotExist for field in module_version.param_fields: if (isinstance(field, ParamSpec.Secret) and field.id_name == param and (isinstance(field.secret_logic, ParamSpec.Secret.Logic.Oauth1a) or isinstance(field.secret_logic, ParamSpec.Secret.Logic.Oauth2))): service_name = field.secret_logic.service service = oauth.OAuthService.lookup_or_none(service_name) if service is None: raise OauthServiceNotConfigured( f'OAuth not configured for "{service_name}" service') return wf_module, service else: raise SecretDoesNotExist( f"Param {param} does not point to an OAuth secret")
def get_workflow_as_delta_and_needs_render(self): """ Return (apply-delta dict, needs_render), or raise Workflow.DoesNotExist needs_render is a (workflow_id, delta_id) pair. """ with Workflow.authorized_lookup_and_cooperative_lock( "read", self.scope["user"], self.scope["session"], pk=self.workflow_id) as workflow_lock: workflow = workflow_lock.workflow request = RequestWrapper(self.scope["user"], self.scope["session"]) ret = { "updateWorkflow": (WorkflowSerializer(workflow, context={ "request": request }).data) } tabs = list(workflow.live_tabs) ret["updateTabs"] = dict( (tab.slug, TabSerializer(tab).data) for tab in tabs) wf_modules = list(WfModule.live_in_workflow(workflow.id)) ret["updateWfModules"] = dict( (str(wfm.id), WfModuleSerializer(wfm).data) for wfm in wf_modules) if workflow.are_all_render_results_fresh(): needs_render = None else: needs_render = (workflow.id, workflow.last_delta_id) return (ret, needs_render)
def _load_wf_module_and_service( workflow: Workflow, wf_module_id: int, param: str) -> Tuple[WfModule, oauth.OAuthService]: """ Load WfModule and OAuthService from the database, or raise. Raise WfModule.DoesNotExist if the WfModule is deleted or missing. Raise SecretDoesNotExist if the WfModule does not have the given param. Invoke this within a Workflow.cooperative_lock(). """ # raises WfModule.DoesNotExist wf_module = WfModule.live_in_workflow(workflow).get(pk=wf_module_id) # raises KeyError, RuntimeError try: module_zipfile = MODULE_REGISTRY.latest(wf_module.module_id_name) except KeyError: raise SecretDoesNotExist( f"Module {wf_module.module_id_name} does not exist") module_spec = module_zipfile.get_spec() for field in module_spec.param_fields: if (isinstance(field, ParamSpec.Secret) and field.id_name == param and (isinstance(field.secret_logic, ParamSpec.Secret.Logic.Oauth1a) or isinstance(field.secret_logic, ParamSpec.Secret.Logic.Oauth2))): service_name = field.secret_logic.service service = oauth.OAuthService.lookup_or_none(service_name) if service is None: raise OauthServiceNotConfigured( f'OAuth not configured for "{service_name}" service') return wf_module, service else: raise SecretDoesNotExist( f"Param {param} does not point to an OAuth secret")
def load_database_objects(workflow_id: int, wf_module_id: int) -> DatabaseObjects: """ Query WfModule info or raise WfModule.DoesNotExist/Workflow.DoesNotExist. Return tuple containing: * wf_module: cjwstate.models.WfModule * module_version: Optional[cjwstate.models.ModuleVersion] * stored_object: Optional[cjwstate.models.StoredObject] * input_cached_render_result: Optional[cjwstate.models.CachedRenderResult] of previous module in tab """ with Workflow.lookup_and_cooperative_lock(id=workflow_id) as workflow_lock: # raise WfModule.DoesNotExist wf_module = WfModule.live_in_workflow(workflow_lock.workflow).get( id=wf_module_id ) try: module_version = ModuleVersion.objects.latest(wf_module.module_id_name) except ModuleVersion.DoesNotExist: module_version = None try: stored_object = wf_module.stored_objects.get( stored_at=wf_module.stored_data_version ) except StoredObject.DoesNotExist: stored_object = None try: # raise WfModule.DoesNotExist -- but we'll catch this one prev_module = wf_module.tab.live_wf_modules.get(order=wf_module.order - 1) input_crr = prev_module.cached_render_result # may be None except WfModule.DoesNotExist: input_crr = None return DatabaseObjects(wf_module, module_version, stored_object, input_crr)
def _get_workflow_as_clientside_update(user, session, workflow_id: int) -> WorkflowUpdateData: """ Return (clientside.Update, delta_id). Raise Workflow.DoesNotExist if a race deletes the Workflow. The purpose of this method is to hide races from users who disconnect and reconnect while changes are being made. It's okay for things to be slightly off, as long as users don't notice. (Long-term, we can build better a more-correct synchronization strategy.) """ with Workflow.authorized_lookup_and_cooperative_lock( "read", user, session, pk=workflow_id) as workflow_lock: workflow = workflow_lock.workflow update = clientside.Update( workflow=workflow.to_clientside(), tabs={tab.slug: tab.to_clientside() for tab in workflow.live_tabs}, steps={ step.id: step.to_clientside() for step in WfModule.live_in_workflow(workflow) }, ) return WorkflowUpdateData(update, workflow.last_delta_id)
def are_all_render_results_fresh(self): """Query whether all live WfModules are rendered.""" from .WfModule import WfModule for wf_module in WfModule.live_in_workflow(self): if wf_module.cached_render_result is None: return False return True
def load_database_objects(workflow_id: int, wf_module_id: int) -> DatabaseObjects: """ Query WfModule info. Raise `WfModule.DoesNotExist` or `Workflow.DoesNotExist` if the step was deleted. Catch a `ModuleError` from migrate_params() and return it as part of the `DatabaseObjects`. """ with Workflow.lookup_and_cooperative_lock(id=workflow_id) as workflow_lock: # raise WfModule.DoesNotExist wf_module = WfModule.live_in_workflow( workflow_lock.workflow).get(id=wf_module_id) # module_zipfile try: module_zipfile = MODULE_REGISTRY.latest(wf_module.module_id_name) except KeyError: module_zipfile = None # migrated_params_or_error if module_zipfile is None: migrated_params_or_error = {} else: try: migrated_params_or_error = cjwstate.params.get_migrated_params( wf_module, module_zipfile=module_zipfile) # raise ModuleError except ModuleError as err: migrated_params_or_error = err # stored_object try: stored_object = wf_module.stored_objects.get( stored_at=wf_module.stored_data_version) except StoredObject.DoesNotExist: stored_object = None # input_crr try: # raise WfModule.DoesNotExist -- but we'll catch this one prev_module = wf_module.tab.live_wf_modules.get( order=wf_module.order - 1) input_crr = prev_module.cached_render_result # may be None except WfModule.DoesNotExist: input_crr = None return DatabaseObjects( wf_module, module_zipfile, migrated_params_or_error, stored_object, input_crr, )
def wrapper(request: HttpRequest, workflow_id: int, wf_module_slug: str, *args, **kwargs): auth_header = request.headers.get("Authorization", "") auth_header_match = AuthTokenHeaderRegex.match(auth_header) if not auth_header_match: return ErrorResponse(403, "authorization-bearer-token-not-provided") bearer_token = auth_header_match.group(1) try: with Workflow.lookup_and_cooperative_lock( id=workflow_id) as workflow_lock: workflow = workflow_lock.workflow try: wf_module = WfModule.live_in_workflow(workflow).get( slug=wf_module_slug) except WfModule.DoesNotExist: return ErrorResponse(404, "step-not-found") try: module_zipfile = MODULE_REGISTRY.latest( wf_module.module_id_name) except KeyError: return ErrorResponse(400, "step-module-deleted") try: file_param_id_name = next( iter(pf.id_name for pf in module_zipfile.get_spec().param_fields if pf.type == "file")) except StopIteration: return ErrorResponse(400, "step-has-no-file-param") api_token = wf_module.file_upload_api_token if not api_token: return ErrorResponse(403, "step-has-no-api-token") bearer_token_hash = hashlib.sha256( bearer_token.encode("utf-8")).digest() api_token_hash = hashlib.sha256( api_token.encode("utf-8")).digest() if bearer_token_hash != api_token_hash or bearer_token != api_token: return ErrorResponse(403, "authorization-bearer-token-invalid") return f( request, workflow_lock, wf_module, file_param_id_name, *args, **kwargs, ) except Workflow.DoesNotExist: return ErrorResponse(404, "workflow-not-found")
def _write_wf_module_position(workflow: Workflow, wf_module_id: int) -> None: """Write position in DB, or raise (Workflow|Tab|WfModule).DoesNotExist.""" with workflow.cooperative_lock(): # raises Workflow.DoesNotExist # Raises WfModule.DoesNotExist, e.g. if tab.is_deleted wf_module = WfModule.live_in_workflow(workflow).get(pk=wf_module_id) tab = wf_module.tab tab.selected_wf_module_position = wf_module.order tab.save(update_fields=["selected_wf_module_position"]) workflow.selected_tab_position = tab.position workflow.save(update_fields=["selected_tab_position"])
def make_init_state(request, workflow=None, modules=None): """ Build a dict to embed as JSON in `window.initState` in HTML. Raise Http404 if the workflow disappeared. Side-effect: update workflow.last_viewed_at. """ ret = {} if workflow: try: with workflow.cooperative_lock(): # raise DoesNotExist on race ret["workflowId"] = workflow.id ret["workflow"] = WorkflowSerializer(workflow, context={ "request": request }).data tabs = list(workflow.live_tabs) ret["tabs"] = dict( (str(tab.slug), TabSerializer(tab).data) for tab in tabs) wf_modules = list(WfModule.live_in_workflow(workflow)) ret["wfModules"] = { str(wfm.id): WfModuleSerializer(wfm).data for wfm in wf_modules } workflow.last_viewed_at = timezone.now() workflow.save(update_fields=["last_viewed_at"]) except Workflow.DoesNotExist: raise Http404("Workflow was recently deleted") if modules: modules_data_list = ModuleSerializer(modules, many=True).data ret["modules"] = dict([(str(m["id_name"]), m) for m in modules_data_list]) if request.user.is_authenticated: ret["loggedInUser"] = UserSerializer(request.user).data return ret
def make_init_state(request, workflow: Workflow, modules: Dict[str, ModuleZipfile]) -> Dict[str, Any]: """ Build a dict to embed as JSON in `window.initState` in HTML. Raise Http404 if the workflow disappeared. Side-effect: update workflow.last_viewed_at. """ try: with workflow.cooperative_lock(): # raise DoesNotExist on race workflow.last_viewed_at = timezone.now() workflow.save(update_fields=["last_viewed_at"]) state = clientside.Init( workflow=workflow.to_clientside(), tabs={ tab.slug: tab.to_clientside() for tab in workflow.live_tabs }, steps={ step.id: step.to_clientside() for step in WfModule.live_in_workflow(workflow) }, modules={ module_id: clientside.Module( spec=module.get_spec(), js_module=module.get_optional_js_module(), ) for module_id, module in modules.items() }, ) except Workflow.DoesNotExist: raise Http404("Workflow was recently deleted") ctx = JsonizeContext(request.user, request.session, request.locale_id, modules) return jsonize_clientside_init(state, ctx)
def _load_wf_module(workflow: Workflow, wf_module_id: int) -> WfModule: """Returns a WfModule or raises HandlerError.""" try: return WfModule.live_in_workflow(workflow).get(id=wf_module_id) except WfModule.DoesNotExist: raise HandlerError("DoesNotExist: WfModule not found")
def _workflow_has_notifications(workflow_id: int) -> bool: """Detect whether a workflow sends email on changes.""" return WfModule.live_in_workflow(workflow_id).filter( notifications=True).exists()