def extract_subjects(subject_info_xml, primary_str):
    """Extract a set of authenticated subjects from a DataONE SubjectInfo.

    - See subject_info_tree for details.

    Args:
        subject_info_xml : str
            A SubjectInfo XML document.

        primary_str : str
            A DataONE subject, typically a DataONE compliant serialization of the DN of
            the DataONE X.509 v3 certificate extension from which the SubjectInfo was
            extracted.

            The primary subject can be viewed as the root of a tree. Any subject in the
            SubjectInfo that is directly or indirectly connected to the root subject is
            included in the returned set of authenticated subjects.


    Returns:
        set: Set of authenticated subjects. Will always include the primary subject.

        - All subjects in the returned set are equivalent to ``primary_str`` for the
          purpose of access control for private science objects.

        - If SubjectInfo does not contain all relevant records, it is still considered
          to be valid, but the authenticated set will be incomplete.

        - Only the subject strings and relationships in SubjectInfo are used by this
          function. Other information about subjects, such as name and email address,
          is ignored.

        - No attempt should be made to infer type of subject from the content of a
          subject string. Subject strings should be handled as random Unicode
          sequences, each of which may designate an person subject, an equivalent
          subject, or a group subject.

        - To determine if an action is authorized, the returned set is checked against
          the authorized_set for a given object. If one or more subjects exist in both
          sets, the action is authorized. The check can be performed with high
          performance using a set union operation in Python or an inner join in
          Postgres.

        - Subject types are only known and relevant while processing the SubjectInfo
          type.
        - The type of each subject in the authenticated_subjects and allowed_subjects
          lists are unknown and irrelevant.

    Notes:
        Procedure:

        The set of authenticated subjects is generated from the SubjectInfo and primary
        subject using the following procedure:

        - Start with empty set of subjects
        - Add authenticatedUser
        - If ``subject`` is not in set of subjects:
        - Add ``subject``
        - Iterate over Person records
        - If Person.subject is ``subject``:
        - If Person.verified is present and set:
        - Add "verifiedUser"
        - Iterate over Person.equivalentIdentity:
        - Recursively add those subjects
        - Iterate over Person.isMemberOf
        - Recursively add those subjects, but ONLY check Group subjects
        - Iterate over Group records
        - If any Group.hasMember is ``subject``:
        - Recursively add Group.subject (not group members)


        Handling of various invalid SubjectInfo and corner cases:

        - SubjectInfo XML doc that is not well formed
        - Return an exception that includes a useful error message with the line number
          of the issue

        - person.isMemberOf and group.hasMember should always form pairs referencing
          each other.
        - One side of the pair is missing
        - Process the available side as normal
        - person.isMemberOf subject references a person or equivalent instead of a
          group
        - Only Group subjects are searched for isMemberOf references, so only the
          referenced Group subject is added to the list of authorized subjects

        - Multiple Person or Group records conflict by using the same subject
        - The records are handled as equivalents

        - person.isMemberOf subject does not reference a known subject
        - If the Person containing the dangling isMemberOf IS NOT connected with the
          authenticated subject, the whole record, including the isMemberOf subject is
          simply ignored
        - If it IS connected with an authenticated subject, the isMemberOf subject is
          authenticated and recursive processing of the subject is skipped

        - Circular references
        - Handled by skipping recursive add for subjects that are already added

        - See the unit tests for example SubjectInfo XML documents for each of these
          issues and the expected results.

    """
    subject_info_pyxb = deserialize_subject_info(subject_info_xml)
    subject_info_tree = gen_subject_info_tree(subject_info_pyxb, primary_str)
    return subject_info_tree.get_subject_set()