Exemplo n.º 1
0
def apply(isim_application: ISIMApplication,
          container_path: str,
          name: str,
          role_classification: str,
          description: Optional[str] = None,
          role_owners: Optional[List[tuple]] = None,
          user_owners: Optional[List[tuple]] = None,
          enable_access: bool = None,
          common_access: bool = None,
          access_type: Optional[str] = None,
          access_image_uri: Optional[str] = None,
          access_search_terms: Optional[List[str]] = None,
          access_additional_info: Optional[str] = None,
          access_badges: Optional[List[Dict[str, str]]] = None,
          assignment_attributes: Optional[List[str]] = None,
          check_mode=False,
          force=False) -> IBMResponse:
    """
    Apply a role configuration. This function will dynamically choose whether to to create or modify based on whether
        a role with the same name exists in the same container. Only attributes which differ from the existing role
        will be changed. Note that the name and container_path of an existing role can't be changed because they are
        used to identify the role. If they don't match an existing role, a new role will be created with the specified
        name and container_path.
    :param isim_application: The ISIMApplication instance to connect to.
    :param container_path: A path representing the container (business unit) that the role exists in. The expected
            format is '//organization_name//profile::container_name//profile::container_name'. Valid values for profile
            are 'ou' (organizational unit), 'bp' (business partner unit), 'lo' (location), or 'ad' (admin domain).
    :param name: The role name.
    :param role_classification: Set to either "application" or "business".
    :param description: A description of the role.
    :param role_owners: A list of tuples containing the container path and role name for each of the roles that
        own this role.
    :param user_owners: A list of tuples containing the container path and uid for each of the users that own
        this role.
    :param enable_access: Set to True to enable access for the role.
    :param common_access: Set to True to show the role as a common access.
    :param access_type: Set to one of 'application', 'emailgroup', 'sharedfolder' or 'role'.
    :param access_image_uri: The URI of an image to use for the access icon.
    :param access_search_terms: A list of search terms for the access.
    :param access_additional_info: Additional information about the acceess.
    :param access_badges: A list of dicts representing badges for the access. Each entry in the list must contain the
        keys 'text' and 'colour' with string values.
    :param assignment_attributes: A list of attribute names to assign to the role.
    :param check_mode: Set to True to enable check mode.
    :param force: Set to True to force execution regardless of current state. This will always result in a new role
        being created, regardless of whether a role with the same name in the same container already exists. Use with
        caution.
    :return: An IBMResponse object. If the call was successful, the data field will contain the Python dict
        representation of the action taken by the server. If a modify request was used, the data field will be empty.
    """

    # Check that the compulsory attributes are set properly
    if not (isinstance(container_path, str) and len(container_path) > 0
            and isinstance(name, str) and len(name) > 0 and isinstance(
                role_classification, str) and len(role_classification) > 0):
        raise ValueError(
            "Invalid role configuration. container_path, name, and role_classification must "
            "have non-empty string values.")

    # If any values are set to None, they must be replaced with empty values. This is because these values will be
    # passed to methods that interpret None as 'no change', whereas we want them to be explicitly set to empty values.
    if description is None:
        description = ""

    if role_owners is None:
        role_owners = []

    if user_owners is None:
        user_owners = []

    if enable_access is None:
        enable_access = False

    if common_access is None:
        common_access = False

    if access_type is None:
        access_type = ""

    if access_image_uri is None:
        access_image_uri = ""

    if access_search_terms is None:
        access_search_terms = []

    if access_additional_info is None:
        access_additional_info = ""

    if access_badges is None:
        access_badges = []

    if assignment_attributes is None:
        assignment_attributes = []

    # Convert the container path into a DN that can be passed to the SOAP API. This also validates the container path.
    dn_encoder = DNEncoder(isim_application)
    container_dn = dn_encoder.container_path_to_dn(container_path)

    # Convert the role and user owner names into DNs that can be passed to the SOAP API
    role_owner_dns = []
    for role_owner in role_owners:
        role_owner_dns.append(
            dn_encoder.encode_to_isim_dn(container_path=str(role_owner[0]),
                                         name=str(role_owner[1]),
                                         object_type='role'))

    user_owner_dns = []
    for user_owner in user_owners:
        user_owner_dns.append(
            dn_encoder.encode_to_isim_dn(container_path=str(user_owner[0]),
                                         name=str(user_owner[1]),
                                         object_type='person'))

    # Resolve the instance with the specified name in the specified container
    existing_role = dn_encoder.get_unique_object(container_path=container_path,
                                                 name=name,
                                                 object_type='role')

    if existing_role is None or force:
        # If the instance doesn't exist yet, create a new role and return the response
        if check_mode:
            return create_return_object(changed=True)
        else:
            return _create(isim_application=isim_application,
                           container_dn=container_dn,
                           name=name,
                           role_classification=role_classification,
                           description=description,
                           role_owner_dns=role_owner_dns,
                           user_owner_dns=user_owner_dns,
                           enable_access=enable_access,
                           common_access=common_access,
                           access_type=access_type,
                           access_image_uri=access_image_uri,
                           access_search_terms=access_search_terms,
                           access_additional_info=access_additional_info,
                           access_badges=access_badges,
                           assignment_attributes=assignment_attributes)
    else:
        # If an existing instance was found, compare it's attributes with the requested attributes and determine if a
        # modify operation is required.
        modify_required = False

        existing_role_classification = get_soap_attribute(
            existing_role, 'erroleclassification')

        if existing_role_classification is None:
            modify_required = True
        elif role_classification.lower() == "application":
            if existing_role_classification[
                    0] != "role.classification.application":
                modify_required = True
            else:
                role_classification = None  # set to None so that no change occurs
        elif role_classification.lower() == "business":
            if existing_role_classification[
                    0] != "role.classification.business":
                modify_required = True
            else:
                role_classification = None  # set to None so that no change occurs
        else:
            raise ValueError(
                role_classification +
                "is not a valid role classification. Must be 'application' or "
                "'business'.")

        existing_description = get_soap_attribute(existing_role, 'description')
        if existing_description is None:
            if description != '':
                modify_required = True
            else:
                description = None  # set to None so that no change occurs
        elif description != existing_role[
                'description'] or description != existing_description[0]:
            modify_required = True
        else:
            description = None  # set to None so that no change occurs

        existing_owners = get_soap_attribute(existing_role, 'owner')
        new_owners = role_owner_dns + user_owner_dns
        if existing_owners is None:
            if new_owners != []:
                modify_required = True
            else:
                # set to None so that no change occurs
                role_owner_dns = None
                user_owner_dns = None
        elif Counter(new_owners) != Counter(existing_owners):
            modify_required = True
        else:
            # set to None so that no change occurs
            role_owner_dns = None
            user_owner_dns = None

        existing_access_setting = get_soap_attribute(existing_role,
                                                     'eraccessoption')
        if existing_access_setting is None:
            modify_required = True
        elif not enable_access and existing_access_setting[0] != '1':
            modify_required = True
        elif enable_access and not common_access and existing_access_setting[
                0] != '2':
            modify_required = True
        elif enable_access and common_access and existing_access_setting[
                0] != '3':
            modify_required = True
        else:
            # set to None so that no change occurs
            enable_access = None
            common_access = None

        existing_access_type = get_soap_attribute(existing_role,
                                                  'erobjectprofilename')

        if existing_access_type is None:
            if access_type != '':
                modify_required = True
            else:
                access_type = None  # set to None so that no change occurs
        elif access_type.lower() == "application":
            if existing_access_type[0] != "Application":
                modify_required = True
            else:
                access_type = None  # set to None so that no change occurs
        elif access_type.lower() == "sharedfolder":
            if existing_access_type[0] != "SharedFolder":
                modify_required = True
            else:
                access_type = None  # set to None so that no change occurs
        elif access_type.lower() == "emailgroup":
            if existing_access_type[0] != "MailGroup":
                modify_required = True
            else:
                access_type = None  # set to None so that no change occurs
        elif access_type.lower() == "role":
            if existing_access_type[0] != "AccessRole":
                modify_required = True
            else:
                access_type = None  # set to None so that no change occurs

        else:
            raise ValueError(
                access_type +
                "is not a valid access type. Must be 'application', 'sharedfolder', "
                "'emailgroup', or 'role'.")

        existing_access_image_uri = get_soap_attribute(existing_role,
                                                       'erimageuri')
        if existing_access_image_uri is None:
            if access_image_uri != '':
                modify_required = True
            else:
                access_image_uri = None  # set to None so that no change occurs
        elif access_image_uri != existing_access_image_uri[0]:
            modify_required = True
        else:
            access_image_uri = None  # set to None so that no change occurs

        existing_access_search_terms = get_soap_attribute(
            existing_role, 'eraccesstag')
        if existing_access_search_terms is None:
            if access_search_terms != []:
                modify_required = True
            else:
                access_search_terms = None  # set to None so that no change occurs
        elif Counter(access_search_terms) != Counter(
                existing_access_search_terms):
            modify_required = True
        else:
            access_search_terms = None  # set to None so that no change occurs

        existing_access_additional_info = get_soap_attribute(
            existing_role, 'eradditionalinformation')
        if existing_access_additional_info is None:
            if access_additional_info != '':
                modify_required = True
            else:
                access_additional_info = None  # set to None so that no change occurs
        elif access_additional_info != existing_access_additional_info[0]:
            modify_required = True
        else:
            access_additional_info = None  # set to None so that no change occurs

        existing_access_badges = get_soap_attribute(existing_role, 'erbadge')

        new_badges = []
        for badge in access_badges:
            new_badges.append(str(badge['text'] + "~" + badge['colour']))

        if existing_access_badges is None:
            if new_badges != []:
                modify_required = True
            else:
                access_badges = None  # set to None so that no change occurs
        elif Counter(new_badges) != Counter(existing_access_badges):
            modify_required = True
        else:
            access_badges = None  # set to None so that no change occurs

        existing_assignment_attributes = get_soap_attribute(
            existing_role, 'erroleassignmentkey')
        if existing_assignment_attributes is None:
            if assignment_attributes != []:
                modify_required = True
            else:
                assignment_attributes = None  # set to None so that no change occurs
        elif Counter(assignment_attributes) != Counter(
                existing_assignment_attributes):
            modify_required = True
        else:
            assignment_attributes = None  # set to None so that no change occurs

        if modify_required:
            if check_mode:
                return create_return_object(changed=True)
            else:
                existing_dn = existing_role['itimDN']

                return _modify(isim_application=isim_application,
                               role_dn=existing_dn,
                               role_classification=role_classification,
                               description=description,
                               role_owner_dns=role_owner_dns,
                               user_owner_dns=user_owner_dns,
                               enable_access=enable_access,
                               common_access=common_access,
                               access_type=access_type,
                               access_image_uri=access_image_uri,
                               access_search_terms=access_search_terms,
                               access_additional_info=access_additional_info,
                               access_badges=access_badges,
                               assignment_attributes=assignment_attributes)
        else:
            return create_return_object(changed=False)
Exemplo n.º 2
0
def apply(isim_application: ISIMApplication,
          parent_container_path: str,
          profile: str,
          name: str,
          description: Optional[str] = None,
          associated_people: Optional[List[tuple]] = None,
          check_mode=False,
          force=False) -> IBMResponse:
    """
    Apply a container configuration. This function will dynamically choose whether to to create or modify based on
        whether a container with the same name and profile exists in the same parent container. Only attributes which
        differ from the existing container will be changed. Note that the name, profile, and parent_container_path of
        an existing container can't be changed because they are used to identify the container. If they don't match an
        existing container, a new container will be created with the specified name, profile, and parent_container_path.
    :param isim_application: The ISIMApplication instance to connect to.
    :param parent_container_path: A path representing the container (business unit) that the container exists in. The
        expected format is '//organization_name//profile::container_name//profile::container_name'. Valid values for
        profile are 'ou' (organizational unit), 'bp' (business partner unit), 'lo' (location), or 'ad' (admin domain).
        To create an Organization, you must specify the root container here, i.e. "//".
    :param profile: The container profile to use. Valid values are 'Organization', 'OrganizationalUnit',
        'BPOrganization', 'Location', or 'AdminDomain'.
    :param name: The container name. This will be automatically applied to the correct attribute depending on the
        container profile selected.
    :param description: A description of the container. Will be ignored if the selected container profile doesn't use
        a description.
    :param associated_people: A list of tuples containing the container path and UID of each of the people associated
        with the container. The way this list will be interpreted changes depending on the selected container profile.
        For an organization, it will be ignored. For an organizational unit or location, the first entry in the list
        will be set as the supervisor, and the other entries will be ignored. For a business partner unit, the first
        entry in the list will be set as the sponsor, and the other entries will be ignored. For an admin domain, each
        entry in the list will be set as an administrator of the admin domain.
    :param check_mode: Set to True to enable check mode.
    :param force: Set to True to force execution regardless of current state. This will always result in a new container
        being created, regardless of whether a container with the same name and profile in the same parent container
        already exists. Use with caution.
    :return: An IBMResponse object. If the call was successful, the data field will contain the Python dict
        representation of the action taken by the server. If a modify request was used, the data field will be empty.
    """

    # Validate the profile name. If performing a modify, make sure it matches the existing profile. In fact, the
    # profile should be used to identify containers. The logic to select the attributes to modify should probably be
    # done in apply, and the create and modify methods just take a list of attributes to set.

    # Check that the compulsory attributes are set properly
    if not (isinstance(parent_container_path, str)
            and len(parent_container_path) > 0 and isinstance(profile, str)
            and len(profile) > 0 and isinstance(name, str) and len(name) > 0):
        raise ValueError(
            "Invalid container configuration. parent_container_path, profile, and name must have "
            "non-empty string values.")

    # Validate the selected profile
    if profile == 'Organization':
        profile_prefix = 'o'
    elif profile == 'OrganizationalUnit':
        profile_prefix = 'ou'
    elif profile == 'BPOrganization':
        profile_prefix = 'bp'
    elif profile == 'Location':
        profile_prefix = 'lo'
    elif profile == 'AdminDomain':
        profile_prefix = 'ad'
    else:
        raise ValueError(
            "'" + profile +
            "' is not a valid container profile. Valid values are 'Organization', "
            "'OrganizationalUnit', 'BPOrganization', 'Location', or 'AdminDomain'."
        )

    # If any values are set to None, they must be replaced with empty values. This is because these values will be
    # passed to methods that interpret None as 'no change', whereas we want them to be explicitly set to empty values.
    if description is None:
        description = ""

    if associated_people is None:
        associated_people = []

    # Convert the parent container path into a DN that can be passed to the SOAP API. This also validates the parent
    # container path.
    dn_encoder = DNEncoder(isim_application)
    parent_container_dn = dn_encoder.container_path_to_dn(
        parent_container_path)

    # Convert the associated people names into DNs that can be passed to the SOAP API
    # We don't perform this step for an organization as an organization cannot have associated people
    associated_people_dns = []
    if profile != "Organization":
        for person in associated_people:
            associated_people_dns.append(
                dn_encoder.encode_to_isim_dn(container_path=str(person[0]),
                                             name=str(person[1]),
                                             object_type='person'))

    # Resolve the instance with the specified name in the specified container
    existing_container = dn_encoder.get_unique_object(
        container_path=parent_container_path,
        name=profile_prefix + "::" + name,
        object_type='container')

    if existing_container is None or force:
        # If the instance doesn't exist yet, create a new role and return the response
        if check_mode:
            return create_return_object(changed=True)
        else:
            ret_obj = _create(isim_application=isim_application,
                              parent_container_dn=parent_container_dn,
                              profile=profile,
                              name=name,
                              description=description,
                              associated_people_dns=associated_people_dns)
            return strip_zeep_element_data(ret_obj)
    else:
        # If an existing instance was found, compare it's attributes with the requested attributes and determine if a
        # modify operation is required.
        modify_required = False

        existing_description = get_soap_attribute(existing_container,
                                                  'description')

        if (profile == "Organization" or profile == "OrganizationalUnit"
                or profile == "Location" or profile == "AdminDomain"):

            if existing_description is None:
                if description != '':
                    modify_required = True
                else:
                    description = None  # set to None so that no change occurs
            elif description != existing_description[0]:
                modify_required = True
            else:
                description = None  # set to None so that no change occurs
        else:
            description = None  # set to None so that no change occurs

        existing_supervisor = get_soap_attribute(existing_container,
                                                 'erSupervisor')
        existing_sponsor = get_soap_attribute(existing_container, 'erSponsor')
        existing_administrators = get_soap_attribute(existing_container,
                                                     'erAdministrator')

        if (profile == "OrganizationalUnit" or profile == "Location"):

            if associated_people_dns == []:
                new_supervisor = ''
            else:
                new_supervisor = associated_people_dns[0]

            if existing_supervisor is None:
                if new_supervisor != '':
                    modify_required = True
                else:
                    associated_people_dns = None  # set to None so that no change occurs
            elif new_supervisor != existing_supervisor[0]:
                modify_required = True
            else:
                associated_people_dns = None  # set to None so that no change occurs

        elif profile == "BPOrganization":
            if associated_people_dns == []:
                new_sponsor = ''
            else:
                new_sponsor = associated_people_dns[0]

            if existing_sponsor is None:
                if new_sponsor != '':
                    modify_required = True
                else:
                    associated_people_dns = None  # set to None so that no change occurs
            elif new_sponsor != existing_sponsor[0]:
                modify_required = True
            else:
                associated_people_dns = None  # set to None so that no change occurs

        elif profile == "AdminDomain":
            if existing_administrators is None:
                if associated_people_dns != []:
                    modify_required = True
                else:
                    associated_people_dns = None  # set to None so that no change occurs
            elif Counter(associated_people_dns) != Counter(
                    existing_administrators):
                modify_required = True
            else:
                associated_people_dns = None  # set to None so that no change occurs

        else:
            associated_people_dns = None  # set to None so that no change occurs

        if modify_required:
            if check_mode:
                return create_return_object(changed=True)
            else:
                existing_dn = existing_container['itimDN']

                ret_obj = _modify(isim_application=isim_application,
                                  container_dn=existing_dn,
                                  profile=profile,
                                  description=description,
                                  associated_people_dns=associated_people_dns)
                return strip_zeep_element_data(ret_obj)
        else:
            return create_return_object(changed=False)
Exemplo n.º 3
0
def apply(isim_application: ISIMApplication,
          container_path: str,
          uid: str,
          profile: str,
          full_name: str,
          surname: str,
          aliases: Optional[List[str]] = None,
          password: Optional[str] = None,
          roles: Optional[List[tuple]] = None,
          check_mode=False,
          force=False) -> IBMResponse:
    """
    Apply a person configuration. This function will dynamically choose whether to to create or modify based on whether
        a person with the same uid exists in the same container. Note that encrypted attribute values such as passwords
        will always be updated because there is no way to determine whether the new value matches the existing one.
        Only attributes which differ from the existing person will be changed. Note that the uid, container_path, and
        profile of an existing person can't be changed. In the uid's case this is because it is used to identify the
        person. If the uid doesn't match an existing person in the specified container, a new person will be created
        with the specified uid and container_path.
    :param isim_application: The ISIMApplication instance to connect to.
    :param container_path: A path representing the container (business unit) that the person exists in. The expected
            format is '//organization_name//profile::container_name//profile::container_name'. Valid values for profile
            are 'ou' (organizational unit), 'bp' (business partner unit), 'lo' (location), or 'ad' (admin domain).
    :param uid: The username or UID for the person.
    :param profile: The name of the profile to use for the person. Currently only "Person" is supported.
    :param full_name: The full name of the person.
    :param surname: The surname of the person.
    :param aliases: A list of aliases for the person.
    :param password: A password to use as the preferred password for the person.
    :param roles: A list of tuples containing the container path and role name for each of the roles that the person
        will be part of.
    :param check_mode: Set to True to enable check mode.
    :param force: Set to True to force execution regardless of current state. This will always result in a new person
        being created, regardless of whether a person with the same full name in the same container already exists. Use
        with caution.
    :return: An IBMResponse object. If the call was successful, the data field will contain the Python dict
        representation of the action taken by the server. If a modify request was used, the data field will be empty.
    """

    # Check that the compulsory attributes are set properly
    if not (isinstance(container_path, str) and len(container_path) > 0
            and isinstance(uid, str) and len(uid) > 0
            and isinstance(profile, str) and len(profile) > 0
            and isinstance(full_name, str) and len(full_name) > 0
            and isinstance(surname, str) and len(surname) > 0):
        raise ValueError(
            "Invalid person configuration. container_path, uid, profile, full_name, and surname "
            "must have non-empty string values.")

    # If any values are set to None, they must be replaced with empty values. This is because these values will be
    # passed to methods that interpret None as 'no change', whereas we want them to be explicitly set to empty values.
    if aliases is None:
        aliases = []

    if password is None:
        password = ""

    if roles is None:
        roles = []

    # Convert the container path into a DN that can be passed to the SOAP API. This also validates the container path.
    dn_encoder = DNEncoder(isim_application)
    container_dn = dn_encoder.container_path_to_dn(container_path)

    # Convert the role names into DNs that can be passed to the SOAP API
    role_dns = []
    for role in roles:
        role_dns.append(
            dn_encoder.encode_to_isim_dn(container_path=str(role[0]),
                                         name=str(role[1]),
                                         object_type='role'))

    # Resolve the instance with the specified name in the specified container
    existing_person = dn_encoder.get_unique_object(
        container_path=container_path, name=uid, object_type='person')

    if existing_person is None or force:
        # If the instance doesn't exist yet, create a new role and return the response
        if check_mode:
            return create_return_object(changed=True)
        else:
            return _create(isim_application=isim_application,
                           container_dn=container_dn,
                           uid=uid,
                           profile=profile,
                           full_name=full_name,
                           surname=surname,
                           aliases=aliases,
                           password=password,
                           role_dns=role_dns)
    else:
        # If an existing instance was found, compare it's attributes with the requested attributes and determine if a
        # modify operation is required.
        modify_required = False

        existing_full_name = get_soap_attribute(existing_person, 'cn')

        if existing_full_name is None:
            modify_required = True
        elif existing_full_name[0] != full_name:
            modify_required = True
        else:
            full_name = None  # set to None so that no change occurs

        existing_surname = get_soap_attribute(existing_person, 'sn')

        if existing_surname is None:
            modify_required = True
        elif existing_surname[0] != surname:
            modify_required = True
        else:
            surname = None  # set to None so that no change occurs

        existing_aliases = get_soap_attribute(existing_person, 'eraliases')

        if existing_aliases is None:
            if aliases != []:
                modify_required = True
            else:
                aliases = None  # set to None so that no change occurs
        elif Counter(aliases) != Counter(existing_aliases):
            modify_required = True
        else:
            aliases = None  # set to None so that no change occurs

        existing_password = get_soap_attribute(existing_person,
                                               'erpersonpassword')

        if existing_password is None:
            if password != "":
                modify_required = True
            else:
                password = None  # set to None so that no change occurs
        elif existing_password[0] != password:
            modify_required = True
        else:
            password = None  # set to None so that no change occurs

        existing_roles = get_soap_attribute(existing_person, 'erroles')

        if existing_roles is None:
            if role_dns != []:
                modify_required = True
            else:
                role_dns = None  # set to None so that no change occurs
        elif Counter(role_dns) != Counter(existing_roles):
            modify_required = True
        else:
            role_dns = None  # set to None so that no change occurs

        if modify_required:
            if check_mode:
                return create_return_object(changed=True)
            else:
                existing_dn = existing_person['itimDN']

                return _modify(isim_application=isim_application,
                               person_dn=existing_dn,
                               full_name=full_name,
                               surname=surname,
                               aliases=aliases,
                               password=password,
                               role_dns=role_dns)
        else:
            return create_return_object(changed=False)
    def dn_to_container_path(self, dn: str) -> str:
        """
        Takes an ISIM DN referring to an ISIM organizational container and converts it to a container path in the format
            '//organization_name//profile::container_name//profile::container_name'. Valid values for profile are 'ou'
            (organizational unit), 'bp' (business partner unit), 'lo' (location), or 'ad' (admin domain). The root
            container (i.e. the parent of all organizations) is specified as "//". This function assumes that all
            containers with the same parent and profile have unique names.
        :param: dn: The ISIM DN to convert.
        :return: The container path of the specified container.
            '//organization_name//profile::container_name//profile::container_name'. Valid values for profile are 'ou'
            (organizational unit), 'bp' (business partner unit), 'lo' (location), or 'ad' (admin domain). The root
            container (i.e. the parent of all organizations) is specified as "//".
        """

        # Check if the DN is the root DN
        if dn == self.isim_application.root_dn:
            return "//"

        container_path = ""
        current_dn = dn

        while True:
            # Look up the DN, add it to the container path, and retrieve it's parent's DN.
            get_response = isimws.isim.container.get(self.isim_application,
                                                     container_dn=current_dn)

            # If an error was encountered and ignored, raise an exception
            if get_response['rc'] != 0:
                raise ValueError(
                    "There was an error while retrieving a container. Return code: "
                    + get_response['rc'])
            result = get_response['data']

            # If the object is not a container, it will be missing expected fields.
            if 'name' not in result or \
                    'profileName' not in result or \
                    'attributes' not in result or \
                    'children' not in result or \
                    'supervisorDN' not in result:
                raise ValueError(
                    "The DN '" + current_dn +
                    "' does not refer to a container. Make sure that the DN is "
                    "correct.")

            if result['profileName'] is None:
                # If the DN doesn't exist, the API will return a result with empty values.
                raise ValueError(
                    "The DN '" + current_dn +
                    "' could not be found in the system. Make sure that the DN "
                    "is correct.")
            elif result['profileName'] == "BPOrganization":
                profile = "bp"
            elif result['profileName'] == "Location":
                profile = "lo"
            elif result['profileName'] == "AdminDomain":
                profile = "ad"
            elif result['profileName'] == "OrganizationalUnit":
                profile = "ou"
            elif result['profileName'] == "Organization":
                return "//" + result["name"] + container_path
            else:
                raise ValueError(
                    "The DN '" + current_dn +
                    "' does not have a valid container profile. Make sure that "
                    "the DN is correct.")

            container_path = "//" + profile + "::" + result[
                'name'] + container_path
            current_dn = get_soap_attribute(result, "erparent")[0]
    def decode_from_isim_dn(self, dn: str) -> Optional[Dict]:
        """
        Takes an ISIM DN referring to an ISIM object and makes it human-readable by retrieving the container
            path, object name, and object type. Supports the following object types: 'role', 'person', 'service', and
            'workflow'. Returns None if the DN does not exist.
        :param: dn: An ISIM DN referring to a non-container object.
        :return: A Dict containing the keys 'container_path', 'name', and 'object_type'.
        """
        if dn is None:
            raise ValueError("You must supply a valid ISIM DN.")

        responses = []
        # Determine the object type
        if ',ou=roles,' in dn:
            object_type = 'role'
            try:
                responses.append(
                    isimws.isim.role.get(
                        isim_application=self.isim_application, role_dn=dn))
            except IBMError as e:
                if "Role does not exist" in e.args[0]:
                    return None
                else:
                    raise e
        elif ',ou=people,' in dn:
            object_type = 'person'
            try:
                responses.append(
                    isimws.isim.person.get(
                        isim_application=self.isim_application, person_dn=dn))
            except IBMError as e:
                if "LDAP: error code 32 - No Such Object" in e.args[0]:
                    return None
                else:
                    raise e
        elif ',ou=services,' in dn:
            object_type = 'service'
            try:
                responses.append(
                    isimws.isim.service.get(
                        isim_application=self.isim_application, service_dn=dn))
            except IBMError as e:
                if "LDAP: error code 32 - No Such Object" in e.args[0]:
                    return None
                else:
                    raise e
        elif ',ou=workflow,' in dn:
            object_type = 'workflow'
            try:
                responses.append(
                    isimws.isim.workflow.get_attribute(
                        isim_application=self.isim_application,
                        workflow_dn=dn,
                        attribute_name='erprocessname'))

                responses.append(
                    isimws.isim.workflow.get_attribute(
                        isim_application=self.isim_application,
                        workflow_dn=dn,
                        attribute_name='erparent'))
            except IBMFatal as e:
                if "Internal Error" in e.args[0]:
                    return None
                else:
                    raise e
        else:
            raise ValueError(
                "ISIM DN " + str(dn) +
                " doesn't match any supported object types. Supported types are "
                "'role', 'person', 'service', and 'workflow'.")

        # Validate responses from the SOAP API
        for response in responses:
            if response['rc'] != 0:
                raise ValueError('Cannot retrieve object information for ' +
                                 str(dn) + ' from the application server.')

        # Process the SOAP API response
        if object_type == 'role':
            result = responses[0]['data']
            object_name = get_soap_attribute(result, "errolename")[0]
            container_dn = get_soap_attribute(result, "erparent")[0]
        elif object_type == 'person':
            result = responses[0]['data']
            object_name = get_soap_attribute(result, "uid")[0]
            container_dn = get_soap_attribute(result, "erparent")[0]
        elif object_type == 'service':
            result = responses[0]['data']
            object_name = get_soap_attribute(result, "erservicename")[0]
            container_dn = get_soap_attribute(result, "erparent")[0]
        elif object_type == 'workflow':
            object_name = responses[0]['data'][0]
            container_dn = responses[1]['data'][0]
        else:
            raise ValueError(
                "ISIM DN " + str(dn) +
                " doesn't match any supported object types. Supported types are "
                "'role', 'person', 'service', and 'workflow'.")

        # Convert the container DN to a container path
        container_path = self.dn_to_container_path(container_dn)

        return {
            'container_path': container_path,
            'name': object_name,
            'object_type': object_type
        }
    def get_unique_object(self, container_path: str, name: str,
                          object_type: str) -> Optional[Dict]:
        """
        Takes a container path, a name and a type referring to an ISIM object and retrieves it. Returns None
            if the specified object doesn't exist, and raises a ValueError if multiple objects exist that meet the
            criteria. Supports the following object types: 'role', 'person', 'service', 'provisioningpolicy', and
            'container'.
        :param: container_path: The container path of the parent container. Should be in the format
            '//organization_name//profile::container_name//profile::container_name'. Valid values for profile are 'ou'
            (organizational unit), 'bp' (business partner unit), 'lo' (location), or 'ad' (admin domain). The root
            container (i.e. the parent of all organizations) is specified as "//".
        :param: name: The name of the object. For a person, this is the uid. For a container, this includes the profile
            prefix. For example a location container called Sydney would be "lo::Sydney". MAKE SURE YOU USE THE EXACT,
            CASE-SENSITIVE NAME.
        :param: object_type: The type of the object. Valid options are 'role', 'person', 'service',
            'provisioningpolicy', and 'container'.
        :return: The object as returned by the SOAP API.
        """
        if container_path is None or \
                name is None or \
                object_type is None:
            raise ValueError(
                "You must supply values for container_path, name, and object_type."
            )

        # Convert the container path to a DN
        container_dn = self.container_path_to_dn(container_path)

        if object_type == "role":
            search_response = isimws.isim.role.search(
                isim_application=self.isim_application,
                container_dn=container_dn,
                ldap_filter="(errolename=" + name + ")")
        elif object_type == "person":
            search_response = isimws.isim.person.search(
                isim_application=self.isim_application,
                ldap_filter="(uid=" + name + ")")
        elif object_type == "service":
            search_response = isimws.isim.service.search(
                isim_application=self.isim_application,
                container_dn=container_dn,
                ldap_filter="(erservicename=" + name + ")")
        elif object_type == "provisioningpolicy":
            search_response = isimws.isim.provisioningpolicy.search(
                isim_application=self.isim_application,
                container_dn=container_dn,
                policy_name=name)
        elif object_type == "container":
            name_components = name.split("::")
            if len(name_components) != 2:
                raise ValueError(
                    str(name) +
                    " is not a valid value for container name. Make sure you include the "
                    "profile prefix component.")
            if name_components[0] == "o":
                profile = "Organization"
            elif name_components[0] == "ou":
                profile = "OrganizationalUnit"
            elif name_components[0] == "bp":
                profile = "BPOrganization"
            elif name_components[0] == "lo":
                profile = "Location"
            elif name_components[0] == "ad":
                profile = "AdminDomain"
            else:
                raise ValueError(
                    name_components[0] +
                    " is not a valid value for a profile prefix. Valid values are "
                    "'o', 'ou', 'bp', 'lo', and 'ad'.")
            name = name_components[1]
            search_response = isimws.isim.container.search(
                isim_application=self.isim_application,
                parent_dn=container_dn,
                container_name=name,
                profile=profile)
        else:
            raise ValueError(
                str(object_type) +
                " is not a valid value for object_type. Valid values are 'role', "
                "'person', 'service', 'provisioningpolicy', and 'container'.")

        if search_response['rc'] != 0:
            raise ValueError(
                "An error was encountered while searching for a " +
                object_type + " with the name " + name + " in " +
                container_path + ".")

        # Filter out results that don't match the exact name or that aren't direct children of the parent.
        # We iterate backwards through the results so we can delete entries as we go without breaking the loop logic
        for index in range(len(search_response['data']) - 1, -1, -1):
            result = search_response['data'][index]

            if object_type != "person":
                if result['name'] != name:
                    del search_response['data'][index]
            else:
                uid_attribute = get_soap_attribute(result, "uid")[0]
                if uid_attribute != name:
                    del search_response['data'][index]

            if object_type != "provisioningpolicy":
                parent_attribute = get_soap_attribute(result, "erparent")[0]
            else:
                parent_attribute = result["organizationalContainer"]["itimDN"]

            if parent_attribute != container_dn:
                del search_response['data'][index]

        # Validate that there is only one result left
        if len(search_response['data']) > 1:
            raise ValueError(
                "Unable to uniquely identify object. More than one " +
                object_type + " was found with the "
                "name " + name + " in " + container_path + ".")
        elif len(search_response['data']) < 1:
            return None
        else:
            return search_response["data"][0]