Ejemplo n.º 1
0
    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)
Ejemplo n.º 2
0
 def delete(self, request: HttpRequest, workflow_id: int):
     try:
         with Workflow.authorized_lookup_and_cooperative_lock(
                 "owner", request.user, request.session,
                 pk=workflow_id) as workflow_lock:
             workflow = workflow_lock.workflow
             workflow.delete()
         return HttpResponse(status=status.HTTP_204_NO_CONTENT)
     except Workflow.DoesNotExist as err:
         if err.args[0] == "owner access denied":
             return JsonResponse(
                 {
                     "message": str(err),
                     "status_code": 403
                 },
                 status=status.HTTP_403_FORBIDDEN,
             )
         else:
             return JsonResponse(
                 {
                     "message": "Workflow not found",
                     "status_code": 404
                 },
                 status=status.HTTP_404_NOT_FOUND,
             )
Ejemplo n.º 3
0
    def delete(self, request: HttpRequest, workflow_id: int):
        try:
            with Workflow.authorized_lookup_and_cooperative_lock(
                    "owner", request.user, request.session,
                    pk=workflow_id) as workflow:
                workflow.delete()

                if workflow.owner_id:
                    # We lock after delete, but it's still correct. DB commits
                    # are atomic: nothing is written yet.
                    lock_user_by_id(workflow.owner_id, for_write=True)
                    user_update = clientside.UserUpdate(
                        usage=query_user_usage(workflow.owner_id))
        except Workflow.DoesNotExist as err:
            if err.args[0] == "owner access denied":
                return JsonResponse(
                    {
                        "message": str(err),
                        "status_code": 403
                    },
                    status=status.FORBIDDEN,
                )
            else:
                return JsonResponse(
                    {
                        "message": "Workflow not found",
                        "status_code": 404
                    },
                    status=status.NOT_FOUND,
                )

        if workflow.owner_id:
            async_to_sync(rabbitmq.send_user_update_to_user_clients)(
                workflow.owner_id, user_update)
        return HttpResponse(status=status.NO_CONTENT)
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
def start_authorize(request: HttpRequest, workflow_id: int, wf_module_id: int,
                    param: str):
    """
    Redirect to the external service's authentication page and write session.

    Return 404 if id_name is not configured (e.g., user asked for
    'google' but OAUTH_SERVICES['google'] does not exist).

    This is not done over Websockets because only an HTTP request can write a
    session cookie. (In general, that is. Let's not marry ourselves to in-DB
    sessions just to use Websockets here.)
    """
    # Type conversions are guaranteed to work because we used URL regexes
    workflow_id = int(workflow_id)
    wf_module_id = int(wf_module_id)
    param = str(param)

    # Validate workflow_id, wf_module_id and param, and return
    # HttpResponseForbidden if they do not match up
    try:
        with Workflow.authorized_lookup_and_cooperative_lock(
                "owner",  # only owner can modify params
                request.user,
                request.session,
                pk=workflow_id,
        ) as workflow_lock:
            workflow = workflow_lock.workflow
            # raises WfModule.DoesNotExist, ModuleVersion.DoesNotExist
            _, service = _load_wf_module_and_service(workflow, wf_module_id,
                                                     param)
    except Workflow.DoesNotExist as err:
        # Possibilities:
        # str(err) = 'owner access denied'
        # str(err) = 'Workflow matching query does not exist'
        return HttpResponseForbidden(str(err))
    except (WfModule.DoesNotExist, ModuleVersion.DoesNotExist):
        return HttpResponseForbidden("Step or parameter was deleted.")
    except SecretDoesNotExist as err:
        return HttpResponseForbidden(str(err))
    except OauthServiceNotConfigured as err:
        return HttpResponseForbidden(str(err))

    try:
        url, state = service.generate_redirect_url_and_state()
    except oauth.TokenRequestDenied:
        return TemplateResponse(request,
                                "oauth_token_request_denied.html",
                                status=403)

    request.session["oauth-flow"] = Scope(
        service_id=service.service_id,
        state=state,
        workflow_id=workflow_id,
        wf_module_id=wf_module_id,
        param=param,
    )._asdict()

    return redirect(url)
Ejemplo n.º 6
0
 def authorize(self, level):
     try:
         with Workflow.authorized_lookup_and_cooperative_lock(
                 level,
                 self.scope["user"],
                 self.scope["session"],
                 pk=self.workflow_id):
             return True
     except Workflow.DoesNotExist:
         return False
Ejemplo n.º 7
0
    def _lookup_requested_workflow_with_auth_and_cooperative_lock(
        self, ) -> ContextManager[DbObjectCooperativeLock]:
        """Either yield the requested workflow, or raise Workflow.DoesNotExist

        Workflow.DoesNotExist means "permission denied" or "workflow does not exist".
        """
        workflow_id_or_secret_id = self.scope["url_route"]["kwargs"][
            "workflow_id_or_secret_id"]
        if isinstance(workflow_id_or_secret_id, int):
            return Workflow.authorized_lookup_and_cooperative_lock(
                "read",
                self.scope["user"],
                self.scope["session"],
                id=workflow_id_or_secret_id,
            )  # raise Workflow.DoesNotExist
        else:
            return Workflow.lookup_and_cooperative_lock(
                secret_id=workflow_id_or_secret_id
            )  # raise Workflow.DoesNotExist
Ejemplo n.º 8
0
def workflow_detail(request, workflow_id, format=None):
    if request.method == "POST":
        workflow = lookup_workflow_for_write(workflow_id, request)

        valid_fields = {"public"}
        if not set(request.data.keys()).intersection(valid_fields):
            return JsonResponse(
                {
                    "message": "Unknown fields: %r" % request.data,
                    "status_code": 400
                },
                status=status.HTTP_400_BAD_REQUEST,
            )
        if "public" in request.data:
            workflow.public = bool(request.data["public"])
            workflow.save(update_fields=["public"])
        return Response(status=status.HTTP_204_NO_CONTENT)

    elif request.method == "DELETE":
        try:
            with Workflow.authorized_lookup_and_cooperative_lock(
                    "owner", request.user, request.session,
                    pk=workflow_id) as workflow_lock:
                workflow = workflow_lock.workflow
                workflow.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)
        except Workflow.DoesNotExist as err:
            if err.args[0] == "owner access denied":
                return JsonResponse(
                    {
                        "message": str(err),
                        "status_code": 403
                    },
                    status=status.HTTP_403_FORBIDDEN,
                )
            else:
                return JsonResponse(
                    {
                        "message": "Workflow not found",
                        "status_code": 404
                    },
                    status=status.HTTP_404_NOT_FOUND,
                )
Ejemplo n.º 9
0
def finish_authorize(request: HttpRequest) -> HttpResponse:
    """
    Set parameter secret to something valid.

    The external service redirects here after _we_ redirect to _it_ in
    start_authorize(). We cannot include pk in the URL (since the external
    service -- e.g., Google -- requires a fixed URL), so we store the pk in
    the session.
    """
    try:
        flow = request.session["oauth-flow"]
    except KeyError:
        return HttpResponseForbidden(
            "Missing auth session. Please try connecting again.")

    try:
        scope = Scope(**flow)
    except TypeError:
        # This would _normally_ be a crash-worthy exception. But there might
        # be sessions in progress as we deploy, [2018-12-21]. TODO nix this
        # `except` to keep our code clean.
        logger.exception("Malformed auth session. Data: %r", flow)
        return HttpResponseForbidden(
            "Malformed auth session. Please try connecting again.")

    service = oauth.OAuthService.lookup_or_none(scope.service_id)
    if not service:
        return HttpResponseNotFound("Service not configured")

    offline_token = service.acquire_refresh_token_or_str_error(
        request.GET, scope.state)
    if isinstance(offline_token, str):
        return HttpResponseForbidden(offline_token)

    username = service.extract_username_from_token(offline_token)

    try:
        with Workflow.authorized_lookup_and_cooperative_lock(
                "owner",  # only owner can modify params
                request.user,
                request.session,
                pk=scope.workflow_id,
        ) as workflow:
            # raises Step.DoesNotExist, ModuleVersion.DoesNotExist
            step, _ = _load_step_and_service(workflow, scope.step_id,
                                             scope.param)
            step.secrets = {
                **step.secrets,
                scope.param: {
                    "name": username,
                    "secret": offline_token
                },
            }
            step.save(update_fields=["secrets"])
    except Workflow.DoesNotExist as err:
        # Possibilities:
        # str(err) = 'owner access denied'
        # str(err) = 'Workflow matching query does not exist'
        return HttpResponseForbidden(str(err))
    except (ModuleVersion.DoesNotExist, Step.DoesNotExist):
        return HttpResponseNotFound("Step or parameter was deleted.")

    update = clientside.Update(
        steps={step.id: clientside.StepUpdate(secrets=step.secret_metadata)})
    async_to_sync(rabbitmq.send_update_to_workflow_clients)(workflow.id,
                                                            update)

    response = HttpResponse(
        b"""<!DOCTYPE html>
            <html lang="en-US">
                <head>
                    <title>Authorized</title>
                </head>
                <body>
                    <p class="success">
                        You have logged in. You may close this window now.
                    </p>
                </body>
            </html>
        """,
        content_type="text/html; charset=utf-8",
    )
    response["Cache-Control"] = "no-cache"
    return response