OpenLDAP

Authenticate using OpenLDAP or Active Directory.

Authentication & Identity Management · LDAP Authentication

Details

IDOpenLDAP
ProviderOpen Source
CategoryAuthentication & Identity Management
From Version5.0.0
Docker Imagedemisto/py3-tools:1.0.0.10895515
Supported ModulesAgentix XSIAM

README

Overview

This integration enables using your OpenLDAP or Active Directory user authentication settings in Cortex XSOAR. Users can log in to Cortex XSOAR with their OpenLDAP or Active Directory username and passwords, and their permissions in Cortex XSOAR will be set according to the groups and mapping set in AD Roles Mapping.

  • For connecting to the LDAP server with TLS connection it is recommended to use this integration instead of the server integration
    Active Directory Authentication.

Use Cases

Use OpenLDAP or Active Directory user authentication groups to set user roles in Cortex XSOAR.

Configure OpenLDAP on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for ‘LDAP Authentication’ (‘OpenLDAP’ or ‘Active Directory Authentication’ should work as well).
  3. Click Add instance to create and configure a new integration instance.
    • Name: a textual name for the integration instance.
    • LDAP Server Vendor (OpenLDAP or Active Directory. Use ‘Auto’ option to determine the vendor automatically.)
    • Server IP or Host Name (e.g., 192.168.0.1)
    • Port. If not specified, default port is 389, or 636 for LDAPS.
    • User DN (e.g cn=admin,ou=users,dc=domain,dc=com)
    • Base DN (e.g. DC=domain,DC=com)
    • Auto populate groups
    • Groups Object Class
    • Groups Unique Identifier Attribute
    • Group Membership Identifier Attribute
    • User Object Class
    • User Unique Identifier Attribute
    • Page size
    • Connection Type (None, SSL or Start TLS)
    • SSL Version (None, TLS, TLSv1, TLSv1_1, TLSv1_2, TLS_CLIENT)
      (The SSL\TLS version to use in SSL or Start TLS connections types. It is recommended to select the TLS_CLIENT option, which auto-negotiate the highest protocol version that both the client and server support, and configure the context client-side connections. For more information please see: ssl.PROTOCOLS).
    • Trust any certificate (not secure)
    • Use system proxy settings
  4. Click Test to validate the URLs, token, and connection.

Additional Information

Steps required for setting AD roles Mapping: (The steps refer to an OpenLDAP server)

  1. Create OpenLDAP child entry of User Account template under wanted Organizational Unit and Posix Group, with uid as part of DN:
    user

  2. Create OpenLDAP child entry of Posix Group template, with created account from step 1 as memberUid:
    group

  3. If using different attributes and class/group templates (different objectClass), customize the following default values in the instance configuration:
    • Groups Object Class
    • Groups Unique Identifier Attribute
    • Group Membership Identifier Attribute
    • User Object Class
    • User Unique Identifier Attribute
  4. Navigate to Settings > USERS AND ROLES > ROLES.

  5. Choose the role.

  6. Add the created group from step 2 to AD Roles Mapping.
    mapping

  7. Login to Cortex XSOAR using uid or full DN and password of the user created in step 1.

Commands

You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

ad-authenticate


Performs a simple bind operation on the LDAP server.

Base Command

ad-authenticate

Input

Argument Name Description Required
username The username for simple authentication. Required
password The password for simple authentication. Required

Context Output

There is no context output for this command.

Command Example

!ad-authenticate username=user password=secret

Human Readable Output

Done

ad-groups


Fetches LDAP groups under a given base DN.

Base Command

ad-groups

Input

Argument Name Description Required
specific-groups A list of group object names to fetch. The list is delimited by a number sign (#). Optional

Context Output

There is no context output for this command.

Command Example

!ad-groups

Human Readable Output

{
    "Controls": null,
    "Entries": [
        {
            "Attributes": [
                {
                    "Name": "primaryGroupToken",
                    "Values": [
                        "111"
                    ]
                }
            ],
            "DN": "CN=Admin,CN=Builtin,DC=Test,DC=Test1"
        },
        {
            "Attributes": [
                {
                    "Name": "primaryGroupToken",
                    "Values": [
                        "222"
                    ]
                }
            ],
            "DN": "CN=Users,CN=Builtin,DC=Test,DC=Test1"
        },
        
    ],
    "Referrals": [
        "ldap://domainTest/CN=Test,DC=Test,DC=Test1",
        "ldap://domainTest2/CN=Test,DC=Test,DC=Test2"
    ]
}

ad-authenticate-and-roles


Performs a simple bind operation on the LDAP server and returns the authenticated user’s groups.

Base Command

ad-authenticate-and-roles

Input

Argument Name Description Required
username The username for simple authentication. Required
password The password for simple authentication. Required
attribute-mail-pull Whether to return the mail attribute. Possible values are: “true”, “false”. Default is “true”. Optional
attribute-mail Mail attribute to return in the response. Default is “mail”. Optional
attribute-name-pull Whether to return the name attribute. Possible values are: “true”, “false”. Default is “true”. Optional
attribute-name Name attribute to return in the response. Default is “name”. Optional
attribute-phone-pull Whether to return the phone attribute. Possible values are: “true”, “false”. Default is “false”. Optional
attribute-phone Phone attribute to return in the response. Default is “mobile”. Optional

Context Output

There is no context output for this command.

Command Example

!ad-authenticate-and-roles username='username' password='password' attribute-phone-pull=true

Human Readable Output

{
    "Controls": [],
    "Entries": [
        {
            "Attributes": [
                {
                    "Name": "memberOf",
                    "Values": [
                        "CN=Domain ,CN=Users,DC=Test,DC=Test1"
                    ]
                },
                {
                    "Name": "name",
                    "Values": [
                        "User Name"
                    ]
                },
                {
                    "Name": "primaryGroupID",
                    "Values": [
                        "111"
                    ]
                },
                {
                    "Name": "mail",
                    "Values": [
                        "username@mail.com"
                    ]
                },
                {
                    "Name": "mobile",
                    "Values": [
                        "555-5555555"
                    ]
                }
            ],
            "DN": "CN=User Name,CN=Users,DC=Test,DC=Test1"
        }
    ],
    "Referrals": [
        "ldap://domainTest/CN=Test,DC=Test,DC=Test1",
        "ldap://domainTest2/CN=Test,DC=Test,DC=Test2"
    ]
}

ad-entries-search


A generic LDAP search command.

Base Command

ad-entries-search

Input

Argument Name Description Required
search_base The location in the DIT where the search will start. Default is the provided Base DN in the configuration. Optional
search_filter A string that describes what you are searching for. When provided with additional filter arguments, the OR join operator is used. If not provided, and no other filter arguments are present, all entries are returned. Search filters are based on assertions. One assertion is a bracketed expression that affirms something about an attribute and its values, as (givenName=John) or (maxRetries>=10). On the server, each assertion resolves to True, False, or Undefined (which is treated as False) for one or more entries in the DIT. Assertions can be grouped in boolean groups where all assertions (and group, specified with &) or at least one assertion (or group, specified with |) must be True. A single assertion can be negated (not group, specified with !). Each group must be bracketed, allowing for recursive filters. Operators allowed in an assertion are = (equal), <= (less than or equal), >= (greater than or equal), =(present), ~= (approximate), and := (extensible). Surprisingly the less than and the greater than operators don’t exist in the LDAP filter syntax. The aproximate and the extensible operators are obscure and seldom used. In an equality filter you can use the character as a wildcard. Optional
search_scope Specifies how broad the search context. ‘BASE’- retrieves attributes of the entry specified in the search_base. ‘LEVEL’- retrieves attributes of the entries contained in the search_base. The base must reference a container object. ‘SUBTREE’ - retrieves attributes of the entries specified in the search_base and all subordinate containers downward. Possible values are: BASE, LEVEL, SUBTREE. Default is SUBTREE. Optional
attributes A comma-separated list of attributes to return in the response. If attributes is ‘none’, no attributes will be returned except the dn. If attributes is ‘all_user_attributes’ or ‘all_operational_attributes’, all user attributes or all operational attributes are returned. If attributes is ‘all’, both user and operational attributes are returned. Possible values are: none, all_user_attributes, all_operational_attributes, all. Default is all. Optional
cn A comma-separated list of CNs to filter the search by (AND operator is used to join them). Optional
uid A comma-separated list of UIDs to filter the search by (AND operator is used to join them). Optional
object_class A comma-separated list of objectClasses to filter the search by (AND operator is used to join them). Optional
description A comma-separated list of descriptions to filter the search by (AND operator is used to join them). Optional
page Page to return. Optional
page_size Number of entries per page. Defaults to 50 (in case only page was provided). Maximum entries per page is 2000. Optional
limit The maximum number of entries to return. Default is 50. Optional

Context Output

The context output will vary depending on the LDAP server scheme configuration and the attributes provided in the command.

Command Example

!ad-entries-search base_dn="dc=openldap,dc=demisto,dc=int" object_class=top,posixAccount search_filter="(creatorsName=cn=ldapadm,dc=openldap,dc=demisto,dc=int)" attributes=all_operational_attributes page=1 page_size=2

Human Readable Output

createTimestamp creatorsName dn entryCSN entryUUID hasSubordinates modifiersName modifyTimestamp structuralObjectClass subschemaSubentry
2019-12-03 11:23:40+00:00 cn=ldapadm,dc=openldap,dc=demisto,dc=int dc=openldap,dc=demisto,dc=int 20191203112340.454387Z#000000#000, # 19f7dd04-aa0b-1039-811c-25103214a95a True cn=ldapadm,dc=openldap,dc=demisto,dc=int 2019-12-03 11:23:40+00:00 domain cn=Subschema
2019-12-03 11:23:40+00:00 cn=ldapadm,dc=openldap,dc=demisto,dc=int cn=ldapadm,dc=openldap,dc=demisto,dc=int 20240708140845.283421Z#000000#000, # 19f8d2c2-aa0b-1039-811d-25103214a95a False cn=ldapadm,dc=openldap,dc=demisto,dc=int 2024-07-08 14:08:45+00:00 organizationalRole cn=Subschema

Configuration parameters

  • ldap_server_vendor — LDAP Server Vendor (required)
  • host — Server IP or Host Name (e.g., 192.168.0.1) (required)
  • port — Port. If not specified, default port is 389, or 636 for LDAPS.
  • credentials — User DN (e.g., cn=admin,ou=users,dc=domain,dc=com) (required)
  • base_dn — Base DN (e.g., DC=domain,DC=com) (required)
  • fetch_groups — Auto populate groups
  • group_filter_class — Groups Object Class
  • group_identifier_attribute — Groups Unique Identifier Attribute
  • member_identifier_attribute — Group Membership Identifier Attribute
  • user_filter_class — User Object Class
  • custom_attributes — User Defined Attributes
  • user_identifier_attribute — User Unique Identifier Attribute
  • page_size — Page size
  • connection_type — Connection Type (required)
  • ssl_version — SSL Version
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (4)

  • ad-authenticate

    Performs a simple bind operation on the LDAP server.

  • ad-authenticate-and-roles

    Performs a simple bind operation on the LDAP server and returns the authenticated user's groups.

  • ad-entries-search

    A generic LDAP search command.

  • ad-groups

    Fetches LDAP groups under a given base DN.

"""
Tests module for the LDAP Authentication integration
"""

import json
import unittest
from unittest.mock import MagicMock, patch

import pytest
from OpenLDAP import LdapClient, entries_paged_search


class Entry:
    def __init__(self):
        self.value = "OpenLDAProotDSE"


class Connection:
    def __init__(self, vendor):
        if vendor == OPENLDAP:
            self.entries = [{"objectClass": Entry()}]
        else:
            self.entries = [{}]

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass

    @staticmethod
    def search(search_base, search_filter, search_scope, attributes):
        return None


OPENLDAP = "OpenLDAP"
ACTIVE_DIRECTORY = "Active Directory"


class TestLDAPClient:
    @pytest.mark.parametrize("vendor", [OPENLDAP, ACTIVE_DIRECTORY])
    def test_ldap_vendor(self, mocker, vendor):
        mocker.patch("OpenLDAP.Connection", return_value=Connection(vendor))
        client = LdapClient({"ldap_server_vendor": "Auto", "host": "server_ip"})
        assert client._ldap_server_vendor == vendor


class TestsActiveDirectory:
    """
    Contains unit tests for functions that deal with Active directory server only.
    """

    def test_parse_ldap_group_entries_and_referrals(self):
        """
        Given:
            - A raw response of a groups paged search in an Active directory server
            (received during the execution of ad-groups command).
        When:
            - Running the 'parse_ldap_group_entries_and_referrals()' function.
        Then:
            - Verify that the raw response parsed correctly and that the referrals and entries lists
              returned as expected.
        """
        client = LdapClient({"ldap_server_vendor": "Active Directory", "host": "server_ip"})

        ldap_group_entries = [
            {"uri": ["ldap://domain1/CN=test,DC=demisto,DC=test"], "type": "searchResRef"},
            {"uri": ["ldap://domain2/DC=test,DC=demisto,DC=test"], "type": "searchResRef"},
            {
                "raw_dn": b"CN=test,CN=Users,DC=demisto,DC=test",
                "dn": "CN=test,CN=Users,DC=demisto,DC=test",
                "raw_attributes": {"primaryGroupToken": [b"11111"]},
                "attributes": {"primaryGroupToken": 11111},
                "type": "searchResEntry",
            },
            {
                "raw_dn": b"CN=DisabledUsers,DC=demisto,DC=test",
                "dn": "CN=DisabledUsers,DC=demisto,DC=test",
                "raw_attributes": {"primaryGroupToken": [b"22222"]},
                "attributes": {"primaryGroupToken": 22222},
                "type": "searchResEntry",
            },
        ]

        expected_referrals = ["ldap://domain1/CN=test,DC=demisto,DC=test", "ldap://domain2/DC=test,DC=demisto,DC=test"]
        expected_entries = [
            {"DN": "CN=test,CN=Users,DC=demisto,DC=test", "Attributes": [{"Name": "primaryGroupToken", "Values": ["11111"]}]},
            {"DN": "CN=DisabledUsers,DC=demisto,DC=test", "Attributes": [{"Name": "primaryGroupToken", "Values": ["22222"]}]},
        ]

        referrals, entries = client._parse_ldap_group_entries_and_referrals(ldap_group_entries)

        assert referrals == expected_referrals
        assert entries == expected_entries

    def test_parse_and_authenticate_ldap_group_entries_and_referrals(self, mocker):
        """
        Given:
            - A raw response of a user paged search in an Active directory server.
            (received during the execution of ad-authenticate-and-roles command).
        When:
            - Running the 'parse_and_authenticate_ldap_group_entries_and_referrals()' function.
        Then:
            - Verify that the raw response parsed correctly and that the referrals and entries lists
              returned as expected.
        """
        client = LdapClient({"ldap_server_vendor": "Active Directory", "host": "server_ip"})

        password = "123456"

        ldap_group_entries = [
            {"uri": ["ldap://domain1/CN=test,DC=demisto,DC=test"], "type": "searchResRef"},
            {"uri": ["ldap://domain2/DC=test,DC=demisto,DC=test"], "type": "searchResRef"},
            {
                "raw_dn": b"CN=username,CN=Users,DC=demisto,DC=test",
                "dn": "CN=username,CN=Users,DC=demisto,DC=test",
                "raw_attributes": {
                    "memberOf": [b"CN=test-group,CN=Users,DC=demisto,DC=test"],
                    "name": [b"username"],
                    "primaryGroupID": [b"111"],
                    "mail": [b"user@mail.com"],
                    "mobile": [b"050-1111111"],
                },
                "attributes": {
                    "memberOf": ["CN=test-group,CN=Users,DC=demisto,DC=test"],
                    "name": "username",
                    "primaryGroupID": 111,
                    "mail": "user@mail.com",
                    "mobile": "050-1111111",
                },
                "type": "searchResEntry",
            },
        ]

        expected_referrals = ["ldap://domain1/CN=test,DC=demisto,DC=test", "ldap://domain2/DC=test,DC=demisto,DC=test"]
        expected_entries = [
            {
                "DN": "CN=username,CN=Users,DC=demisto,DC=test",
                "Attributes": [
                    {"Name": "memberOf", "Values": ["CN=test-group,CN=Users,DC=demisto,DC=test"]},
                    {"Name": "name", "Values": ["username"]},
                    {"Name": "primaryGroupID", "Values": ["111"]},
                    {"Name": "mail", "Values": ["user@mail.com"]},
                    {"Name": "mobile", "Values": ["050-1111111"]},
                ],
            }
        ]

        mocker.patch("OpenLDAP.LdapClient.authenticate_ldap_user", return_value="Done")
        referrals, entries = client._parse_and_authenticate_ldap_group_entries_and_referrals(ldap_group_entries, password)

        assert referrals == expected_referrals
        assert entries == expected_entries

    @pytest.mark.parametrize(
        "user_logon_name, expected_ad_username",
        [
            ("DEMISTO\\test1user", "test1user"),
            ("test2user@demisto.ad", "test2user"),
            ("mertuz ", "mertuz"),
            (" mertuz", "mertuz"),
            ("  mertuz  ", "mertuz"),
            ("DEMISTO\\test1user ", "test1user"),
            (" DEMISTO\\test1user", "test1user"),
            ("test2user@demisto.ad ", "test2user"),
            (" test2user@demisto.ad", "test2user"),
        ],
    )
    def test_get_ad_username(self, user_logon_name, expected_ad_username):
        """
        Given:
            - A user logon name (a username to login to XSOAR with), possibly with surrounding whitespace.
        When:
            - Running the 'get_ad_username()' function.
        Then:
            - Verify that the returned Active Directory username is as expected and free of surrounding whitespace.
        """
        client = LdapClient({"ldap_server_vendor": "Active Directory", "host": "server_ip"})

        ad_username = client._get_ad_username(user_logon_name)
        assert ad_username == expected_ad_username

    @pytest.mark.parametrize(
        "connection_type, expected_auto_bind_value", [("Start TLS", "TLS_BEFORE_BIND"), ("SSL", "NO_TLS"), ("None", "NO_TLS")]
    )
    def test_get_auto_bind_value(self, connection_type, expected_auto_bind_value):
        """
        Given:
            - A connection type:
                1. Start TLS
                2. SSL
                3. None
        When:
            - Running the '_get_auto_bind_value()' function.
        Then:
            - Verify that the returned auto_bind value is as expected:
                1. 'TLS_BEFORE_BIND' - which means that connection should upgrade it's secure level to TLS before
                                       the bind itself (STARTTLS command is executed).
                2. 'NO_TLS' - The connection is secured from the beginning,
                              thus STARTTLS command shouldn't be executed.
                3. 'NO_TLS' - Connection is insecure (cleartext) and shouldn't be upgraded to TLS.
        """
        client = LdapClient({"ldap_server_vendor": "Active Directory", "host": "server_ip", "connection_type": connection_type})

        auto_bind_value = client._get_auto_bind_value()
        assert auto_bind_value == expected_auto_bind_value


class TestsOpenLDAP:
    """
    Contains unit tests for functions that deal with OpenLDAP server only.
    """

    @pytest.mark.parametrize(
        "dn, user_identifier_attribute, expected_result",
        [
            ("uid=user_test,cn=users_test,dc=openldap_test,dc=test,dc=int", "uid", (True, "user_test")),
            ("not_a_valid_dn_test", "uid", (False, "not_a_valid_dn_test")),
        ],
    )
    def test_is_valid_dn(self, dn, user_identifier_attribute, expected_result):
        """
        Given:
            - A DN and a user identifier attribute:
              1. A valid DN.
              2. Invalid DN.
        When:
            - Running the '_is_valid_dn()' function.
        Then:
            - Verify that the DN is parsed correctly and that the user returned as expected.
        """
        client = LdapClient(
            {
                "ldap_server_vendor": "OpenLDAP",
                "host": "server_ip",
                "connection_type": "SSL",
                "user_identifier_attribute": user_identifier_attribute,
            }
        )

        actual_result, dn = client._is_valid_dn(dn, client.USER_IDENTIFIER_ATTRIBUTE)

        assert (actual_result, dn) == expected_result

    @pytest.mark.parametrize(
        "dn, user_identifier_attribute",
        [
            ("cn=users_test,dc=openldap_test,dc=test,dc=int", "uid"),
            ("uid=user_test,cn=users_test,dc=openldap_test,dc=test,dc=int", "new_uid"),
        ],
    )
    def test_is_valid_dn_user_id_not_in_dn(self, dn, user_identifier_attribute):
        """
        Given:
            1. A DN without a user identifier attribute.
            2. A DN with a wrong user identifier attribute.
        When:
            - Running the '_is_valid_dn()' function.
        Then:
            - Verify that the expected err message is raised.
        """
        client = LdapClient(
            {
                "ldap_server_vendor": "OpenLDAP",
                "host": "server_ip",
                "connection_type": "SSL",
                "user_identifier_attribute": user_identifier_attribute,
            }
        )

        with pytest.raises(Exception) as e:
            client._is_valid_dn(dn, client.USER_IDENTIFIER_ATTRIBUTE)
        assert e.value.args[0] == f"OpenLDAP {user_identifier_attribute} attribute was not found in user DN : {dn}"


class TestLDAPAuthentication:
    """
    Contains unit tests for general functions that deal with both OpenLDAP and Active Directory servers.
    """

    @pytest.mark.parametrize(
        "ssl_version, expected_ssl_version",
        [("TLS", 2), ("TLSv1", 3), ("TLSv1_1", 4), ("TLSv1_2", 5), ("TLS_CLIENT", 16), (None, None), ("None", None)],
    )
    def test_get_ssl_version(self, ssl_version, expected_ssl_version):
        """
        Given:
            - An ssl protocol version:
                1. TLS
                2. TLSv1
                3. TLSv1_1
                4. TLSv1_2
                5. TLS_CLIENT
                6. None
                7. 'None'
        When:
            - Running the '_get_ssl_version()' function.
        Then:
            - Verify that the returned ssl version value is as expected:
                1. TLS - 2
                2. TLSv1 - 3
                3. TLSv1_1 - 4
                4. TLSv1_2 - 5
                5. TLS_CLIENT - 16
                6. None - None
                7. 'None' - None
        """
        client = LdapClient(
            {"ldap_server_vendor": "OpenLDAP", "host": "server_ip", "connection_type": "SSL", "ssl_version": ssl_version}
        )

        ssl_version_value = client._get_ssl_version()
        assert ssl_version_value == expected_ssl_version

    @pytest.mark.parametrize(
        "custom_attributes, expected_formatted_attributes",
        [("attr1=val1,attr2=val2,attr3=val3", "(attr1=val1)(attr2=val2)(attr3=val3)"), ("", "")],
    )
    def test_get_formatted_custom_attributes(self, custom_attributes, expected_formatted_attributes):
        """
        Given:
            - Custom attributes:
              1. A valid comma separated list of attributes.
              2. An empty string of attributes.
        When:
            - Running the '_get_formatted_custom_attributes()' function.
        Then:
            - Verify that the attributed parsed correctly.
        """
        client = LdapClient(
            {
                "ldap_server_vendor": "OpenLDAP",
                "host": "server_ip",
                "connection_type": "SSL",
                "custom_attributes": custom_attributes,
            }
        )

        formatted_attributes = client._get_formatted_custom_attributes()
        assert formatted_attributes == expected_formatted_attributes

    def test_get_formatted_custom_attributes_invalid_attributes_input(self):
        """
        Given:
            - Invalid Custom attributes.
        When:
            - Running the '_get_formatted_custom_attributes()' function.
        Then:
            - Verify that the expected error message is raised.
        """
        client = LdapClient(
            {
                "ldap_server_vendor": "OpenLDAP",
                "host": "server_ip",
                "connection_type": "SSL",
                "custom_attributes": "attr1val1,attr2=val2,attr3=val3",
            }
        )

        with pytest.raises(Exception) as e:
            client._get_formatted_custom_attributes()
        assert e.value.args[0] == (
            f'User defined attributes must be of the form "attrA=valA,attrB=valB,...", but got: {client.CUSTOM_ATTRIBUTE}'
        )

    @pytest.mark.parametrize(
        "user_logon_name",
        [
            ("test*"),
            ("test?test"),
        ],
    )
    def test_has_wildcards_in_user_logon(self, user_logon_name):
        """
        Given:
            1. A user logon name contains the "*" symbol.
            2. A user logon name contains the "?" symbol.
        When:
            - Running the 'has_wildcards_in_user_logon()' function.
        Then:
            - Verify that an exception is raised due to the use of wildcards in the logon name.
        """
        client = LdapClient({"ldap_server_vendor": "Active Directory", "host": "server_ip"})

        with pytest.raises(Exception) as e:
            client._has_wildcards_in_user_logon(user_logon_name)
        assert "Wildcards were detected in the user logon name" in e.value.args[0]
        assert user_logon_name in e.value.args[0]


class TestEntriesPagedSearch(unittest.TestCase):
    def setUp(self):
        """
        Set up the test by creating a mock connection and search parameters.
        """
        self.connection = MagicMock()
        self.search_params = {"search_base": "dc=example,dc=com", "search_filter": "(objectClass=person)"}
        self.page_size = 10

    def test_first_page(self):
        """
        when running the entries_paged_search function with a page number of 1 then the search method should be called with
        the correct parameters and the results should be returned.
        """
        self.connection.search.return_value = [{"name": "John Doe"}]

        results = entries_paged_search(self.connection, self.search_params, page=1, page_size=self.page_size)

        self.connection.search.assert_called_once_with(**self.search_params, paged_size=self.page_size)
        assert results == [{"name": "John Doe"}]

    def test_subsequent_page(self):
        """
        when running the entries_paged_search function with a page number greater than 1 then the search method should be called
        with the correct parameters and the results should be returned. The search method should be called twice, once to skip
        the results and once to get the actual results.
        """
        # Mock the connection's search method for the first search (to skip results)
        self.connection.search.side_effect = [
            None,  # First call returns None to simulate the skip search
            [{"name": "Jane Doe"}],  # Second call returns the actual results
        ]
        # Mock the result of the first search to include the cookie
        self.connection.result = {"controls": {"1.2.840.113556.1.4.319": {"value": {"cookie": b"cookie_value"}}}}

        results = entries_paged_search(self.connection, self.search_params, page=2, page_size=self.page_size)

        # Assert the search method was called twice with the correct parameters
        assert self.connection.search.call_count == 2
        self.connection.search.assert_any_call(**self.search_params, paged_size=self.page_size * 1)
        self.connection.search.assert_any_call(**self.search_params, paged_size=self.page_size, paged_cookie=b"cookie_value")
        assert results == [{"name": "Jane Doe"}]


class TestEntriesSearchCommand(unittest.TestCase):
    """
    Test class for the entries_search_command function.
    """

    def setUp(self):
        """
        Set up the test by creating an instance of the LdapClient class and mocking the _get_auto_bind_value method.
        """
        self.instance = LdapClient(
            {
                "host": "server_ip",
                "port": "636",
                "credentials": {"identifier": "username", "password": "password"},
                "base_dn": "dc=example,dc=com",
                "connection_type": "SSL",
                "ssl_version": "TLSv1_2",
                "fetch_groups": True,
                "insecure": False,
                "ldap_server_vendor": "OpenLDAP",
            }
        )

        self.instance._get_auto_bind_value = MagicMock(return_value=True)

    @patch("OpenLDAP.Connection")
    @patch("OpenLDAP.create_entries_search_filter", return_value="(objectClass=*)")
    @patch("OpenLDAP.get_search_attributes", return_value=["cn", "mail"])
    def test_entries_search_command_first_page(
        self, mock_get_search_attributes, mock_create_entries_search_filter, mock_connection
    ):
        """
        when running the entries_search_command function with a page number of 1 then the search method should be called with
        the correct parameters and the results should be returned.
        """
        # Mock the LDAP connection
        mock_conn_instance = mock_connection.return_value.__enter__.return_value
        mock_conn_instance.entries = [
            MagicMock(
                entry_to_json=MagicMock(
                    return_value=json.dumps(
                        {
                            "attributes": {"cn": ["John Doe"], "mail": ["john.doe@example.com"]},
                            "dn": "cn=John Doe,dc=example,dc=com",
                        }
                    )
                )
            )
        ]

        args = {"search_base": "dc=example,dc=com", "search_scope": "SUBTREE", "page": "1", "page_size": "50"}

        result = self.instance.entries_search_command(args)

        self.instance._get_auto_bind_value.assert_called_once()
        mock_create_entries_search_filter.assert_called_once_with(args)
        mock_get_search_attributes.assert_called_once_with("all")
        mock_connection.assert_called_once_with(
            self.instance._ldap_server, self.instance._username, self.instance._password, auto_bind=True
        )

        assert len(result.outputs) == 1
        assert result.outputs[0]["cn"] == ["John Doe"]
        assert result.outputs[0]["mail"] == ["john.doe@example.com"]
        assert result.outputs[0]["dn"] == "cn=John Doe,dc=example,dc=com"

    @patch("OpenLDAP.Connection")
    @patch("OpenLDAP.create_entries_search_filter", return_value="(objectClass=*)")
    @patch("OpenLDAP.get_search_attributes", return_value=["cn", "mail"])
    def test_entries_search_command_subsequent_page(
        self, mock_get_search_attributes, mock_create_entries_search_filter, mock_connection
    ):
        """
        when running the entries_search_command function with a page number greater than 1 then the search method should be
        called with the correct parameters and the results should be returned. The search method should be called twice, once
        to skip the results and once to get the actual results.
        """
        # Mock the LDAP connection
        mock_conn_instance = mock_connection.return_value.__enter__.return_value
        mock_conn_instance.entries = [
            MagicMock(
                entry_to_json=MagicMock(
                    return_value=json.dumps(
                        {
                            "attributes": {"cn": ["Jane Doe"], "mail": ["jane.doe@example.com"]},
                            "dn": "cn=Jane Doe,dc=example,dc=com",
                        }
                    )
                )
            )
        ]

        args = {"search_base": "dc=example,dc=com", "search_scope": "SUBTREE", "page": "2", "page_size": "50"}

        result = self.instance.entries_search_command(args)

        self.instance._get_auto_bind_value.assert_called_once()
        mock_create_entries_search_filter.assert_called_once_with(args)
        mock_get_search_attributes.assert_called_once_with("all")
        mock_connection.assert_called_once_with(
            self.instance._ldap_server, self.instance._username, self.instance._password, auto_bind=True
        )

        assert len(result.outputs) == 1
        assert result.outputs[0]["cn"] == ["Jane Doe"]
        assert result.outputs[0]["mail"] == ["jane.doe@example.com"]
        assert result.outputs[0]["dn"] == "cn=Jane Doe,dc=example,dc=com"