Example #1
0
def admin_remove_phab_project(revision_phid: str,
                              project_phid: str,
                              comment: Optional[str] = None):
    """Remove a project tag from the provided revision.

    Note, this uses administrator privileges and should only be called
    if permissions checking is handled elsewhere.

    Args:
        revision_phid: phid of the revision to remove the project tag from.
        project_phid: phid of the project to remove.
        comment: An optional comment to add when removing the project.
    """
    transactions = [{"type": "projects.remove", "value": [project_phid]}]
    if comment is not None:
        transactions.append({"type": "comment", "value": comment})

    privileged_phab = PhabricatorClient(
        current_app.config["PHABRICATOR_URL"],
        current_app.config["PHABRICATOR_ADMIN_API_KEY"],
    )
    # We only retry for PhabricatorCommunicationException, rather than the
    # base PhabricatorAPIException to treat errors in this implementation as
    # fatal.
    privileged_phab.call_conduit(
        "differential.revision.edit",
        objectIdentifier=revision_phid,
        transactions=transactions,
    )
Example #2
0
def collate_reviewer_attachments(reviewers, reviewers_extra):
    """Return collated reviewer data.

    Args:
        reviewers: Data from the 'reviewers' attachment of
            differential.revision.search.
        reviewers_extra: Data from the 'reviewers-extra'
            attachment of differential.revision.search.
    """
    phids = {}
    for reviewer in reviewers:
        data = {}
        for k in ('reviewerPHID', 'isBlocking', 'actorPHID'):
            data[k] = PhabricatorClient.expect(reviewer, k)

        data['status'] = ReviewerStatus.from_status(
            PhabricatorClient.expect(reviewer, 'status')
        )

        phids[data['reviewerPHID']] = data

    for reviewer in reviewers_extra:
        data = {}
        for k in ('reviewerPHID', 'diffPHID', 'voidedPHID'):
            data[k] = PhabricatorClient.expect(reviewer, k)

        data.update(phids.get(data['reviewerPHID'], {}))
        phids[data['reviewerPHID']] = data

    if len(phids) > min(len(reviewers), len(reviewers_extra)):
        raise PhabricatorCommunicationException(
            'Phabricator responded with unexpected data'
        )

    return phids
Example #3
0
def create_approval_request(phab: PhabricatorClient, revision: dict, form_content: str):
    """Update an existing revision with reviewers & form comment"""
    release_managers = get_release_managers(phab)

    rev = phab.call_conduit(
        "differential.revision.edit",
        objectIdentifier=revision["phid"],
        transactions=[
            # Set release managers as reviewers
            {"type": "reviewers.add", "value": [release_managers["phid"]]},
            # Post the form as a comment on the revision
            {"type": "comment", "value": form_content},
        ],
    )
    rev_id = phab.expect(rev, "object", "id")
    rev_phid = phab.expect(rev, "object", "phid")
    assert rev_id == revision["id"], "Revision id mismatch"

    logger.info("Updated Phabricator revision", extra={"id": rev_id, "phid": rev_phid})

    return {
        "mode": "approval",
        "url": f"{phab.url_base}/D{rev_id}",
        "revision_id": rev_id,
        "revision_phid": rev_phid,
    }
def get_landable_repos_for_revision_data(revision_data, supported_repos):
    """Return a dictionary mapping string PHID to landable Repo

    Args:
        revision_data: A RevisionData.
        supported_repos: A dictionary mapping repository shortname to
        a Repo for repositories lando supports.

    Returns:
        A dictionary where each key is a string PHID for a repository from
        revision_data and the value is a Repo taken from supported_repos.
        Repositories in revision_data which are unsupported will not be
        present in the dictionary.
    """
    repo_phids = {
        PhabricatorClient.expect(revision, "fields", "repositoryPHID")
        for revision in revision_data.revisions.values()
        if PhabricatorClient.expect(revision, "fields", "repositoryPHID")
    }
    repos = {
        phid: supported_repos.get(
            PhabricatorClient.expect(
                revision_data.repositories[phid], "fields", "shortName"
            )
        )
        for phid in repo_phids
        if phid in revision_data.repositories
    }
    return {phid: repo for phid, repo in repos.items() if repo}
Example #5
0
def warning_previously_landed(*, revision, diff, **kwargs):
    revision_id = PhabricatorClient.expect(revision, "id")
    diff_id = PhabricatorClient.expect(diff, "id")

    landed_transplant = (
        Transplant.revisions_query([revision_id])
        .filter_by(status=TransplantStatus.landed)
        .order_by(Transplant.updated_at.desc())
        .first()
    )

    if landed_transplant is None:
        return None

    landed_diff_id = landed_transplant.revision_to_diff_id[str(revision_id)]
    same = diff_id == landed_diff_id
    only_revision = len(landed_transplant.revision_order) == 1

    return (
        "Already landed with {is_same_string} diff ({landed_diff_id}), "
        "pushed {push_string} {commit_sha}.".format(
            is_same_string=("the same" if same else "an older"),
            landed_diff_id=landed_diff_id,
            push_string=("as" if only_revision else "with new tip"),
            commit_sha=landed_transplant.result,
        )
    )
Example #6
0
def _render_author_response(phid, user_search_data):
    author = user_search_data.get(phid, {})
    return {
        'phid': PhabricatorClient.expect(author, 'phid'),
        'username': PhabricatorClient.expect(author, 'fields', 'username'),
        'real_name': PhabricatorClient.expect(author, 'fields', 'realName'),
    }
Example #7
0
def serialize_author(phid, user_search_data):
    out = {"phid": phid, "username": None, "real_name": None}
    author = user_search_data.get(phid)
    if author is not None:
        out["username"] = PhabricatorClient.expect(author, "fields", "username")
        out["real_name"] = PhabricatorClient.expect(author, "fields", "realName")

    return out
Example #8
0
def check_landing_blockers(
        auth0_user,
        requested_path,
        stack_data,
        landable_paths,
        landable_repos,
        *,
        user_blocks=[user_block_no_auth0_email, user_block_scm_level]):
    revision_path = []
    revision_to_diff_id = {}
    for revision_phid, diff_id in requested_path:
        revision_path.append(revision_phid)
        revision_to_diff_id[revision_phid] = diff_id

    # Check that the provided path is a prefix to, or equal to,
    # a landable path.
    for path in landable_paths:
        if revision_path == path[:len(revision_path)]:
            break
    else:
        return TransplantAssessment(
            blocker="The requested set of revisions are not landable.")

    # Check the requested diffs are the latest.
    for revision_phid in revision_path:
        latest_diff_phid = PhabricatorClient.expect(
            stack_data.revisions[revision_phid], "fields", "diffPHID")
        latest_diff_id = PhabricatorClient.expect(
            stack_data.diffs[latest_diff_phid], "id")

        if latest_diff_id != revision_to_diff_id[revision_phid]:
            return TransplantAssessment(
                blocker="A requested diff is not the latest.")

    # Check if there is already a landing for something in the stack.
    if (Transplant.revisions_query([
            PhabricatorClient.expect(r, "id")
            for r in stack_data.revisions.values()
    ]).filter_by(status=TransplantStatus.submitted).first() is not None):
        return TransplantAssessment(blocker=(
            "A landing for revisions in this stack is already in progress."))

    # To be a landable path the entire path must have the same
    # repository, so we can get away with checking only one.
    repo = landable_repos[stack_data.revisions[revision_path[0]]["fields"]
                          ["repositoryPHID"]]

    # Check anything that would block the current user from
    # landing this.
    for block in user_blocks:
        result = block(auth0_user=auth0_user, landing_repo=repo)
        if result is not None:
            return TransplantAssessment(blocker=result)

    return TransplantAssessment()
Example #9
0
 def check(cls, *, get_revision, get_diff, **kwargs):
     diff, _ = get_diff()
     revision = get_revision()
     if (PhabricatorClient.expect(revision, 'phid') !=
             PhabricatorClient.expect(diff, 'fields', 'revisionPHID')):
         raise ProblemException(
             400,
             'Diff not related to the revision',
             'The requested diff is not related to the requested revision.',
             type='https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400'  # noqa
         )  # yapf: disable
Example #10
0
def lazy_get_reviewers(revision):
    """Return a dictionary mapping phid to collated reviewer attachment data.

    Args:
        revision: A dict of the revision data from differential.revision.search
            with the 'reviewers' and 'reviewers-extra' attachments.
    """
    attachments = PhabricatorClient.expect(revision, 'attachments')
    return collate_reviewer_attachments(
        PhabricatorClient.expect(attachments, 'reviewers', 'reviewers'),
        PhabricatorClient.expect(attachments, 'reviewers-extra',
                                 'reviewers-extra'))
Example #11
0
def get_raw_comments(transaction):
    """Return a list of 'raw' comment bodies in a Phabricator transaction.

    A single transaction can have multiple comment bodies: e.g. a top-level comment
    and a couple of inline comments along with it.

    See https://phabricator.services.mozilla.com/conduit/method/transaction.search/.
    """
    return [
        PhabricatorClient.expect(comment, "content", "raw")
        for comment in PhabricatorClient.expect(transaction, "comments")
    ]
Example #12
0
def serialize_author(phid, user_search_data):
    out = {
        'phid': phid,
        'username': None,
        'real_name': None,
    }
    author = user_search_data.get(phid)
    if author is not None:
        out['username'] = PhabricatorClient.expect(author, 'fields',
                                                   'username')
        out['real_name'] = PhabricatorClient.expect(author, 'fields',
                                                    'realName')

    return out
Example #13
0
def _render_diff_response(querydiffs_data):
    return {
        'id': int(PhabricatorClient.expect(querydiffs_data, 'id')),
        'date_created': _epoch_to_isoformat_time(
            PhabricatorClient.expect(querydiffs_data, 'dateCreated')
        ),
        'date_modified': _epoch_to_isoformat_time(
            PhabricatorClient.expect(querydiffs_data, 'dateModified')
        ),
        'author': {
            'name': querydiffs_data.get('authorName', ''),
            'email': querydiffs_data.get('authorEmail', ''),
        },
    }  # yapf: disable
Example #14
0
def _convert_path_id_to_phid(path, stack_data):
    mapping = {
        PhabricatorClient.expect(r, "id"): PhabricatorClient.expect(r, "phid")
        for r in stack_data.revisions.values()
    }
    try:
        return [(mapping[r], d) for r, d in path]
    except IndexError:
        ProblemException(
            400,
            "Landing Path Invalid",
            "The provided landing_path is not valid.",
            type="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400",
        )
Example #15
0
def _assess_transplant_request(phab, landing_path):
    nodes, edges = _find_stack_from_landing_path(phab, landing_path)
    stack_data = request_extended_revision_data(phab, [phid for phid in nodes])
    landing_path = _convert_path_id_to_phid(landing_path, stack_data)

    supported_repos = get_repos_for_env(current_app.config.get('ENVIRONMENT'))
    landable_repos = get_landable_repos_for_revision_data(
        stack_data, supported_repos)
    landable, blocked = calculate_landable_subgraphs(
        stack_data,
        edges,
        landable_repos,
        other_checks=DEFAULT_OTHER_BLOCKER_CHECKS)

    assessment = check_landing_blockers(
        g.auth0_user,
        landing_path,
        stack_data,
        landable,
        landable_repos,
    )
    if assessment.blocker is not None:
        return (assessment, None, None, None)

    # We have now verified that landable_path is valid and is indeed
    # landable (in the sense that it is a landable_subgraph, with no
    # revisions being blocked). Make this clear by using a different
    # value, and assume it going forward.
    valid_path = landing_path

    # Now that we know this is a valid path we can convert it into a list
    # of (revision, diff) tuples.
    to_land = [stack_data.revisions[r_phid] for r_phid, _ in valid_path]
    to_land = [
        (r, stack_data.diffs[PhabricatorClient.expect(r, 'fields',
                                                      'diffPHID')])
        for r in to_land
    ]

    # To be a landable path the entire path must have the same
    # repository, so we can get away with checking only one.
    repo = stack_data.repositories[to_land[0][0]['fields']['repositoryPHID']]
    landing_repo = landable_repos[repo['phid']]

    involved_phids = set()
    for revision, _ in to_land:
        involved_phids.update(gather_involved_phids(revision))

    involved_phids = list(involved_phids)
    users = lazy_user_search(phab, involved_phids)()
    projects = lazy_project_search(phab, involved_phids)()
    reviewers = {
        revision['phid']: get_collated_reviewers(revision)
        for revision, _ in to_land
    }

    assessment = check_landing_warnings(g.auth0_user, to_land, repo,
                                        landing_repo, reviewers, users,
                                        projects)
    return (assessment, to_land, landing_repo, stack_data)
Example #16
0
def test_find_txn_with_comment_in_phabricator(phabdouble):
    phab = phabdouble.get_phabricator_client()
    # A sec-approval request adds a comment to a revision.
    mock_comment = phabdouble.comment("my sec-approval request")
    revision = phabdouble.revision()

    # Add the two sec-approval request transactions to Phabricator. This also links the
    # comment transaction to the revision.
    comment_txn = phabdouble.api_object_for(
        phabdouble.transaction("comment", revision, comments=[mock_comment])
    )
    review_txn = phabdouble.api_object_for(
        phabdouble.transaction("reviewers.add", revision)
    )

    # Fetch our comment transaction
    comment = PhabricatorClient.single(comment_txn, "comments")

    # Add the sec-approval request transactions to the database.
    revision = phabdouble.api_object_for(revision)
    sec_approval_request = SecApprovalRequest.build(revision, [comment_txn, review_txn])

    # Search the list of sec-approval transactions for the comment.
    matching_comment = search_sec_approval_request_for_comment(
        phab, sec_approval_request
    )

    assert matching_comment == comment
Example #17
0
def check_author_planned_changes(*, revision, **kwargs):
    status = RevisionStatus.from_status(
        PhabricatorClient.expect(revision, "fields", "status", "value"))
    if status is not RevisionStatus.CHANGES_PLANNED:
        return None

    return "The author has indicated they are planning changes to this revision."
Example #18
0
 def get_client(api_key=None):
     api_key = (
         api_key or current_app.config['PHABRICATOR_UNPRIVILEGED_API_KEY']
     )
     return PhabricatorClient(
         current_app.config['PHABRICATOR_URL'], api_key
     )
def test_integrated_secure_stack_has_alternate_commit_message(
    db,
    client,
    phabdouble,
    mock_repo_config,
    secure_project,
    authed_headers,
    monkeypatch,
):
    sanitized_title = "my secure commit title"
    revision_title = "my insecure revision title"

    # Build a revision with an active sec-approval request.
    diff, secure_revision = _make_sec_approval_request(
        sanitized_title,
        revision_title,
        authed_headers,
        client,
        monkeypatch,
        phabdouble,
        secure_project,
    )

    # Request the revision from Lando. It should have our new title and summary.
    response = client.get("/stacks/D{}".format(secure_revision["id"]))
    assert response == 200

    revision = PhabricatorClient.single(response.json, "revisions")
    assert revision["is_secure"]
    assert revision["is_using_secure_commit_message"]
    assert revision["title"] == sanitized_title
    assert revision["summary"] == ""
Example #20
0
def warning_not_accepted(*, revision, **kwargs):
    status = RevisionStatus.from_status(
        PhabricatorClient.expect(revision, "fields", "status", "value"))
    if status is RevisionStatus.ACCEPTED:
        return None

    return status.output_name
Example #21
0
        def wrapped(*args, **kwargs):
            api_key = request.headers.get("X-Phabricator-API-Key")

            if api_key is None and not self.optional:
                return problem(
                    401,
                    "X-Phabricator-API-Key Required",
                    ("Phabricator api key not provided in "
                     "X-Phabricator-API-Key header"),
                    type=
                    "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401",
                )

            g.phabricator = PhabricatorClient(
                current_app.config["PHABRICATOR_URL"],
                api_key
                or current_app.config["PHABRICATOR_UNPRIVILEGED_API_KEY"],
            )
            if api_key is not None and not g.phabricator.verify_api_token():
                return problem(
                    403,
                    "X-Phabricator-API-Key Invalid",
                    "Phabricator api key is not valid",
                    type=
                    "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403",
                )

            return f(*args, **kwargs)
Example #22
0
def transaction_search(phabricator,
                       object_identifier,
                       transaction_phids=None,
                       limit=100):
    """Yield the Phabricator transactions related to an object.

    See https://phabricator.services.mozilla.com/conduit/method/transaction.search/.

    If the transaction list is larger that one page of API results then the generator
    will call the Phabricator API successive times to fetch the full transaction list.

    Args:
        phabricator: A PhabricatorClient instance.
        object_identifier: An object identifier (PHID or monogram) whose transactions
            we want to fetch.
        transaction_phids: An optional list of specific transactions PHIDs we want to
            find for the given object_identifier.
        limit: Integer keyword, limit the number of records retrieved per API call.
            Default is 100 records.

    Returns:
        Yields individual transactions.
    """
    next_page_start = None

    if transaction_phids:
        constraints = {"phids": transaction_phids}
    else:
        constraints = {}

    while True:
        transactions = phabricator.call_conduit(
            "transaction.search",
            objectIdentifier=object_identifier,
            constraints=constraints,
            limit=limit,
            after=next_page_start,
        )

        yield from PhabricatorClient.expect(transactions, "data")

        next_page_start = PhabricatorClient.expect(transactions, "cursor",
                                                   "after")

        if next_page_start is None:
            # This was the last page of results.
            return
def reviewer_identity(phid, user_search_data, project_search_data):
    if phid in user_search_data:
        return ReviewerIdentity(
            PhabricatorClient.expect(user_search_data, phid, "fields",
                                     "username"),
            PhabricatorClient.expect(user_search_data, phid, "fields",
                                     "realName"),
        )

    if phid in project_search_data:
        name = PhabricatorClient.expect(project_search_data, phid, "fields",
                                        "name")
        return ReviewerIdentity(name, name)

    logger.info("reviewer was missing from user / project search data",
                extra={"phid": phid})
    return ReviewerIdentity("<unknown>", "Unknown User/Project")
Example #24
0
def select_diff_author(diff):
    commits = PhabricatorClient.expect(diff, "attachments", "commits", "commits")
    if not commits:
        return None, None

    authors = [c.get("author", {}) for c in commits]
    authors = Counter((a.get("name"), a.get("email")) for a in authors)
    authors = authors.most_common(1)
    return authors[0][0] if authors else (None, None)
Example #25
0
def lazy_get_revision_status(revision):
    """Return a landoapi.phabricator.RevisionStatus.

    Args:
        revision: A dict of the revision data just as it is returned
            by Phabricator.
    """
    return RevisionStatus.from_status(
        PhabricatorClient.expect(revision, 'fields', 'status', 'value'))
Example #26
0
def _get_comment(phabdouble, msg):
    """Retrieve the Phabricator API representation of a raw comment string."""
    revision = phabdouble.revision()
    mock_comment = phabdouble.comment(msg)
    phabdouble.transaction("dummy", revision, comments=[mock_comment])
    transaction = phabdouble.api_object_for(
        phabdouble.transaction("dummy", revision, comments=[mock_comment])
    )
    comment = PhabricatorClient.single(transaction, "comments")
    return comment
Example #27
0
def serialize_status(revision):
    status_value = PhabricatorClient.expect(revision, "fields", "status", "value")
    status = RevisionStatus.from_status(status_value)

    if status is RevisionStatus.UNEXPECTED_STATUS:
        logger.warning(
            "Revision had unexpected status",
            extra={
                "id": PhabricatorClient.expection(revision, "id"),
                "value": status_value,
            },
        )
        return {"closed": False, "value": None, "display": "Unknown"}

    return {
        "closed": status.closed,
        "value": status.value,
        "display": status.output_name,
    }
Example #28
0
def reviewer_identity(phid, user_search_data, project_search_data):
    if phid in user_search_data:
        return ReviewerIdentity(
            PhabricatorClient.expect(user_search_data, phid, 'fields',
                                     'username'),
            PhabricatorClient.expect(user_search_data, phid, 'fields',
                                     'realName'))

    if phid in project_search_data:
        name = PhabricatorClient.expect(project_search_data, phid, 'fields',
                                        'name')
        return ReviewerIdentity(name, name)

    logger.info(
        {
            'msg': 'A reviewer was missing from user / project search data',
            'phid': phid,
        }, 'reviewer.unknown')
    return ReviewerIdentity('<unknown>', 'Unknown User/Project')
Example #29
0
def check_phabricator():
    try:
        PhabricatorClient(
            current_app.config['PHABRICATOR_URL'],
            current_app.config['PHABRICATOR_UNPRIVILEGED_API_KEY']
        ).call_conduit('conduit.ping')
    except PhabricatorAPIException as exc:
        return ['PhabricatorAPIException: {!s}'.format(exc)]

    return []
Example #30
0
def heartbeat():
    """Perform an in-depth service health check.

    This should check all the services that this service depends on
    and return a 200 iff those services and the app itself are
    performing normally. Return a 5XX if something goes wrong.
    """
    phab = PhabricatorClient(
        current_app.config['PHABRICATOR_URL'],
        current_app.config['PHABRICATOR_UNPRIVILEGED_API_KEY'])
    try:
        phab.call_conduit('conduit.ping')
    except PhabricatorAPIException:
        logger.warning({
            'msg': 'problem connecting to Phabricator',
        }, 'heartbeat')
        return 'heartbeat: problem', 502
    logger.info({'msg': 'ok, all services are up'}, 'heartbeat')
    return 'heartbeat: ok', 200