def revisions_handler(revision_id, diff_id=None): landoapi = LandoAPIClient( landoapi_url=current_app.config['LANDO_API_URL'], phabricator_api_token=get_phabricator_api_token(), auth0_access_token=session.get('access_token')) # Loads the submitted form if POST or creates a new one if GET form = RevisionForm() errors = [] # Submit the landing request if this is a POST if form.is_submitted(): if not is_user_authenticated(): errors.append('You must be logged in to land a revision.') elif form.validate(): try: # Returns True or raises a LandingSubmissionError if landoapi.post_landings(revision_id, form.diff_id.data, form.confirmation_token.data): redirect_url = ( '/revisions/{revision_id}/{diff_id}/'.format( revision_id=revision_id, diff_id=diff_id)) return redirect(redirect_url) except LandingSubmissionError as e: errors.append(e.error) else: for field, field_errors in form.errors.items(): for error in field_errors: errors.append(error) # If this is a GET or the POST fails, load data to display revision page. revision = landoapi.get_revision(revision_id, diff_id) diff_id = diff_id or revision['diff']['id'] landing_statuses = landoapi.get_landings(revision_id) dryrun_result = {} if is_user_authenticated(): dryrun_result = landoapi.post_landings_dryrun(revision_id, diff_id) form.confirmation_token.data = dryrun_result.get('confirmation_token') form.diff_id.data = diff_id return render_template('revision/revision.html', revision=revision, landing_statuses=landing_statuses, form=form, warnings=dryrun_result.get('warnings', []), blockers=dryrun_result.get('blockers', []), errors=errors)
def revisions_handler(revision_id, diff_id=None): if not is_user_authenticated(): handler = _revisions_handler else: handler = _revisions_handler_with_auth return handler(revision_id, diff_id=diff_id)
def wrapped(*args, **kwargs): if not is_user_authenticated(): handler = no_auth_f else: handler = auth_f return handler(*args, **kwargs)
def home(): enable_transplant_ui = current_app.config.get("ENABLE_EMBEDDED_TRANSPLANT_UI") if not (enable_transplant_ui and is_user_authenticated()): # Return a static HTML page for users that are not logged in. return render_template("home.html") # Render the landing queue. return render_template("queue/queue.html")
def sec_approval_request_handler(): if not current_app.config.get("ENABLE_SEC_APPROVAL"): abort(404) if not is_user_authenticated(): errors = make_form_error( 'You must be logged in to request sec-approval' ) return jsonify(errors=errors), 401 token = get_phabricator_api_token() if not token: # The user has not set their API Token. Lando API will return an # error if it is missing. errors = make_form_error( 'You must set your Phabricator API token in the Lando User ' 'Settings to request sec-approval' ) return jsonify(errors=errors), 400 api = LandoAPI( current_app.config['LANDO_API_URL'], auth0_access_token=session.get('access_token'), phabricator_api_token=token, ) form = SecApprovalRequestForm() if not form.validate(): return jsonify(errors=form.errors), 400 else: logger.info( "sec-approval requested", extra={"revision_id": form.revision_id.data} ) # NOTE: We let errors in the upstream service get turned into # exceptions and bubble up to the application's default exception # handler. It will generate a HTTP 500 for the UI to handle. api.request( "POST", "requestSecApproval", require_auth0=True, json={ "revision_id": form.revision_id.data, "sanitized_message": form.new_message.data, } ) return jsonify({})
def settings(): if not is_user_authenticated(): # Accessing it unauthenticated from UI is protected by CSP return jsonify( dict(success=False, errors=dict(form_errors=["User is not authenticated"])) ) form = UserSettingsForm() if not form.validate_on_submit(): return jsonify(dict(success=False, errors=form.errors)) payload = dict(success=True) response = manage_phab_api_token_cookie(form, payload) return response
def oidc_error(error=None, error_description=None): """Handles authentication errors returned by Auth0. When something goes wrong with authentication, Auth0 redirects to our provided redirect uri (simply /redirect_uri when using flask_pyoidc) with the above two query parameters: error and error_description. We hook into this using the @oidc.error_view decorator so we can handle recoverable errors. The most common error is with refreshing the user's session automatically, officially called "Silent Authentication" (see https://auth0.com/docs/api-auth/tutorials/silent-authentication). With silent authentication enabled, flask_pyoidc passes 'prompt=none' when it requests authentication. If the user's greater Single Sign On session is still active, then the user is logged in seamlessly and their lando-ui session is refreshed. When the user's greater Single Sign On session is expired, Auth0 explicitly raises a 'login_required' error and provides that at our redirect_uri. Auth0 requires that the login is requested _without_ the 'prompt=none' option. By clearing the current session, flask_pyoidc knows to request login with a prompt. In general, when something goes wrong, we logout the user so that they can try again from a fresh state. If the user wasn't logged in to begin with, then we display an error message and log it. """ if ( is_user_authenticated() or error in ('login_required', 'interaction_required') ): # last_local_referrer is guaranteed to not be a signin/signout route. redirect_url = session.get('last_local_referrer') or '/' session.clear() response = make_response(redirect(redirect_url)) response.delete_cookie('phabricator-api-token') return response else: logger.error( 'authentication error', extra={ 'error': error, 'error_description': error_description } ) # yapf: disable raise UIError( title='Authentication Error: {}'.format(error), message=error_description )
def update_landing_job(landing_job_id): if not is_user_authenticated(): errors = make_form_error( "You must be logged in to update a landing job.") return jsonify(errors=errors), 401 token = get_phabricator_api_token() api = LandoAPI( current_app.config["LANDO_API_URL"], auth0_access_token=session.get("access_token"), phabricator_api_token=token, ) try: data = api.request( "PUT", f"landing_jobs/{landing_job_id}", require_auth0=True, json=request.get_json(), ) except LandoAPIError as e: return e.response, e.response["status"] return data
def is_user_authenticated(): return helpers.is_user_authenticated()
def revision(revision_id): api = LandoAPI(current_app.config['LANDO_API_URL'], auth0_access_token=session.get('access_token'), phabricator_api_token=get_phabricator_api_token()) form = TransplantRequestForm() errors = [] if form.is_submitted(): if not is_user_authenticated(): errors.append('You must be logged in to request a landing') elif not form.validate(): for _, field_errors in form.errors.items(): errors.extend(field_errors) else: try: api.request('POST', 'transplants', require_auth0=True, json={ 'landing_path': json.loads(form.landing_path.data), 'confirmation_token': form.confirmation_token.data, }) # We don't actually need any of the data from the # the submission. As long as an exception wasn't # raised we're successful. return redirect( url_for('revisions.revision', revision_id=revision_id)) except LandoAPIError as e: if not e.detail: raise errors.append(e.detail) # Request the entire stack. try: stack = api.request('GET', 'stacks/D{}'.format(revision_id)) except LandoAPIError as e: if e.status_code == 404: raise RevisionNotFound(revision_id) else: raise # Build a mapping from phid to revision and identify # the data for the revision used to load this page. revision = None revisions = {} for r in stack['revisions']: revisions[r['phid']] = r if r['id'] == 'D{}'.format(revision_id): revision = r['phid'] # Request all previous transplants for the stack. transplants = api.request( 'GET', 'transplants', params={'stack_revision_id': 'D{}'.format(revision_id)}) # TODO: support displaying the full DAG, and landing *past* the # current revision. # # The revision may appear in many `landable_paths`` if it has # multiple children, or any of its landable descendents have # multiple children. That being said, there should only be a # single unique path up to this revision, so find the first # it appears in. The revisions up to the target one in this # path form the landable series. series = None for p in stack['landable_paths']: try: series = p[:p.index(revision) + 1] break except ValueError: pass dryrun = None if series and is_user_authenticated(): landing_path = [{ 'revision_id': revisions[phid]['id'], 'diff_id': revisions[phid]['diff']['id'], } for phid in series] form.landing_path.data = json.dumps(landing_path) dryrun = api.request('POST', 'transplants/dryrun', require_auth0=True, json={'landing_path': landing_path}) form.confirmation_token.data = dryrun.get('confirmation_token') series = list(reversed(series)) return render_template( 'stack/stack.html', revision_id='D{}'.format(revision_id), series=series, dryrun=dryrun, stack=stack, transplants=transplants, revisions=revisions, revision_phid=revision, errors=errors, form=form, )
def revision(revision_id): api = LandoAPI( current_app.config["LANDO_API_URL"], auth0_access_token=session.get("access_token"), phabricator_api_token=get_phabricator_api_token(), ) form = TransplantRequestForm() sec_approval_form = SecApprovalRequestForm() errors = [] if form.is_submitted(): if not is_user_authenticated(): errors.append("You must be logged in to request a landing") elif not form.validate(): for _, field_errors in form.errors.items(): errors.extend(field_errors) else: try: api.request( "POST", "transplants", require_auth0=True, json={ "landing_path": json.loads(form.landing_path.data), "confirmation_token": form.confirmation_token.data, }, ) # We don't actually need any of the data from the # the submission. As long as an exception wasn't # raised we're successful. return redirect(url_for("revisions.revision", revision_id=revision_id)) except LandoAPIError as e: if not e.detail: raise errors.append(e.detail) # Request the entire stack. try: stack = api.request("GET", "stacks/D{}".format(revision_id)) except LandoAPIError as e: if e.status_code == 404: raise RevisionNotFound(revision_id) else: raise # Build a mapping from phid to revision and identify # the data for the revision used to load this page. revision = None revisions = {} for r in stack["revisions"]: revisions[r["phid"]] = r if r["id"] == "D{}".format(revision_id): revision = r["phid"] # Build a mapping from phid to repository. repositories = {} for r in stack["repositories"]: repositories[r["phid"]] = r # Request all previous transplants for the stack. transplants = api.request( "GET", "transplants", params={"stack_revision_id": "D{}".format(revision_id)} ) # The revision may appear in many `landable_paths`` if it has # multiple children, or any of its landable descendents have # multiple children. That being said, there should only be a # single unique path up to this revision, so find the first # it appears in. The revisions up to the target one in this # path form the landable series. # # We also form a set of all the revisions that are landable # so we can present selection for what to land. series = None landable = set() for p in stack["landable_paths"]: for phid in p: landable.add(phid) try: series = p[: p.index(revision) + 1] except ValueError: pass dryrun = None target_repo = None if series and is_user_authenticated(): landing_path = [ { "revision_id": revisions[phid]["id"], "diff_id": revisions[phid]["diff"]["id"], } for phid in series ] form.landing_path.data = json.dumps(landing_path) dryrun = api.request( "POST", "transplants/dryrun", require_auth0=True, json={"landing_path": landing_path}, ) form.confirmation_token.data = dryrun.get("confirmation_token") series = list(reversed(series)) target_repo = repositories.get(revisions[series[0]]["repo_phid"]) phids = set(revisions.keys()) edges = set(Edge(child=e[0], parent=e[1]) for e in stack["edges"]) order = sort_stack_topological( phids, edges, key=lambda x: int(revisions[x]["id"][1:]) ) drawing_width, drawing_rows = draw_stack_graph(phids, edges, order) annotate_sec_approval_workflow_info(revisions) # Are we showing the "sec-approval request submitted" dialog? # If we are then fill in its values. submitted_revision = request.args.get("show_approval_success") submitted_rev_url = None if submitted_revision: for rev in revisions.values(): if rev["id"] == submitted_revision: submitted_rev_url = rev["url"] break return render_template( "stack/stack.html", revision_id="D{}".format(revision_id), series=series, landable=landable, dryrun=dryrun, stack=stack, rows=list(zip(reversed(order), reversed(drawing_rows))), drawing_width=drawing_width, transplants=transplants, revisions=revisions, revision_phid=revision, sec_approval_form=sec_approval_form, submitted_rev_url=submitted_rev_url, target_repo=target_repo, errors=errors, form=form, )
def revision(revision_id): api = LandoAPI(current_app.config['LANDO_API_URL'], auth0_access_token=session.get('access_token'), phabricator_api_token=get_phabricator_api_token()) form = TransplantRequestForm() errors = [] if form.is_submitted(): if not is_user_authenticated(): errors.append('You must be logged in to request a landing') elif not form.validate(): for _, field_errors in form.errors.items(): errors.extend(field_errors) else: try: api.request('POST', 'transplants', require_auth0=True, json={ 'landing_path': json.loads(form.landing_path.data), 'confirmation_token': form.confirmation_token.data, }) # We don't actually need any of the data from the # the submission. As long as an exception wasn't # raised we're successful. return redirect( url_for('revisions.revision', revision_id=revision_id)) except LandoAPIError as e: if not e.detail: raise errors.append(e.detail) # Request the entire stack. try: stack = api.request('GET', 'stacks/D{}'.format(revision_id)) except LandoAPIError as e: if e.status_code == 404: raise RevisionNotFound(revision_id) else: raise # Build a mapping from phid to revision and identify # the data for the revision used to load this page. revision = None revisions = {} for r in stack['revisions']: revisions[r['phid']] = r if r['id'] == 'D{}'.format(revision_id): revision = r['phid'] # Build a mapping from phid to repository. repositories = {} for r in stack['repositories']: repositories[r['phid']] = r # Request all previous transplants for the stack. transplants = api.request( 'GET', 'transplants', params={'stack_revision_id': 'D{}'.format(revision_id)}) # The revision may appear in many `landable_paths`` if it has # multiple children, or any of its landable descendents have # multiple children. That being said, there should only be a # single unique path up to this revision, so find the first # it appears in. The revisions up to the target one in this # path form the landable series. # # We also form a set of all the revisions that are landable # so we can present selection for what to land. series = None landable = set() for p in stack['landable_paths']: for phid in p: landable.add(phid) try: series = p[:p.index(revision) + 1] except ValueError: pass dryrun = None target_repo = None if series and is_user_authenticated(): landing_path = [{ 'revision_id': revisions[phid]['id'], 'diff_id': revisions[phid]['diff']['id'], } for phid in series] form.landing_path.data = json.dumps(landing_path) dryrun = api.request('POST', 'transplants/dryrun', require_auth0=True, json={'landing_path': landing_path}) form.confirmation_token.data = dryrun.get('confirmation_token') series = list(reversed(series)) target_repo = repositories.get(revisions[series[0]]['repo_phid']) phids = set(revisions.keys()) edges = set(Edge(child=e[0], parent=e[1]) for e in stack['edges']) order = sort_stack_topological(phids, edges, key=lambda x: int(revisions[x]['id'][1:])) drawing_width, drawing_rows = draw_stack_graph(phids, edges, order) return render_template( 'stack/stack.html', revision_id='D{}'.format(revision_id), series=series, landable=landable, dryrun=dryrun, stack=stack, rows=list(zip(reversed(order), reversed(drawing_rows))), drawing_width=drawing_width, transplants=transplants, revisions=revisions, revision_phid=revision, target_repo=target_repo, errors=errors, form=form, )