Source code for pyad.adgroup

from typing import List
from .adobject import ADObject
from . import pyadutils
from . import pyadconstants


class ADGroup(ADObject):
    """
    The class representing Active Directory groups and the accessors methods specific
    to groups.
    """

    @classmethod
    def create(
        cls,
        name,
        container_object,
        security_enabled=True,
        scope="GLOBAL",
        optional_attributes={},
    ):
        """Creates and returns a new group"""
        return container_object.create_group(
            name=name,
            security_enabled=security_enabled,
            scope=scope,
            optional_attributes=optional_attributes,
        )

    def add_members(self, members):
        """
        Accepts a list of pyAD objects or a single pyAD object and adds as members to the
        group.
        """

        members = [member.dn for member in pyadutils.generate_list(members)]
        self.append_to_attribute("member", members)

    def remove_members(self, members: ADObject) -> None:
        """
        Accepts a list of pyAD objects or a single pyAD object and removes these as
        members from the group.
        """

        members = [member.dn for member in pyadutils.generate_list(members)]
        self.remove_from_attribute("member", members)

    def remove_all_members(self) -> None:
        """Removes all members of the group."""

        self.remove_from_attribute("member", self.get_attribute("member"))

    def get_members(
        self, recursive: bool = False, ignore_groups: bool = False
    ) -> List[ADObject]:
        """
        Returns a list of group members.

        :param recursive: Whether to recursively traverse through nested groups, defaults
            to False
        :type recursive: bool, optional
        :param ignore_groups: include groups in the list, defaults to False
        :type ignore_groups: bool, optional
        :return: _description_
        :rtype: List[ADObject]
        """

        return self._get_members(recursive, ignore_groups)

    def _get_members(
        self,
        recursive: bool,
        ignore_groups: bool = False,
        processedGroups: List[str] = [],
    ) -> List[ADObject]:
        """
        returns a list of pyAD objects that are members of the group.

        :param recursive: Whether to recursively traverse through nested groups, defaults
            to False
        :type recursive: bool
        :param ignore_groups: include groups in the list, defaults to False
        :type ignore_groups: bool, optional
        :param processedGroups: A list of object IDs that have already been processed,
            defaults to []
        :type processedGroups: List[str]
        :return: The members of the group
        :rtype: List[ADObject]
        """

        processedGroups.append(self.guid)
        # we need to keep track of which groups have been enumerated so far so that
        # we don't enter an infinite loop accidentally if group A is a member
        # of group B and group B is a member of group A. Yes, this can actually happen.
        ret = []
        for dn in self.get_attribute("member"):
            pyADobj = ADObject(dn, options=self._make_options())
            pyADobj.adjust_pyad_type()
            if pyADobj.type == "group" and pyADobj.guid not in processedGroups:
                if recursive:
                    ret.extend(
                        pyADobj._get_members(
                            recursive=True,
                            ignoreGroups=ignore_groups,
                            processedGroups=processedGroups,
                        )
                    )
                if not ignore_groups:
                    ret.append(pyADobj)
            elif pyADobj.type != "group":
                ret.append(pyADobj)
        return list((set(ret)))  # converting to set removes duplicates

    def sync_membership(self, new_population: List[ADObject]) -> None:
        """
        Synchronizes membership of group so that it matches the list of entries in new_population

        :param new_population: The list of entries to match against the group
        :type new_population: List[ADObject]
        """

        current_members = set(self.get_members())
        new_population = set(new_population)
        self.add_members(list(new_population - current_members))
        self.remove_members(list(current_members - new_population))

    def check_contains_member(
        self, check_member: ADObject, recursive: bool = False
    ) -> bool:
        """
        Checks whether the check_member is a member of the group

        :param check_member: The object to check for membership of the current group object
        :type check_member: ADObject
        :param recursive: Whether to recursively check membership or only check direct
            members, defaults to False
        :type recursive: bool, optional
        :return: Whether the check_member is a member of the group
        :rtype: bool
        """

        if check_member in self.get_members(recursive=recursive, ignoreGroups=False):
            return True
        else:
            return False

    def get_group_scope(self) -> str:
        """Returns the group scope GLOBAL, UNIVERSAL, or LOCAL."""
        group_type = self.get_attribute("groupType", False)
        c_global = pyadconstants.ADS_GROUP_TYPE["GLOBAL"]
        c_universal = pyadconstants.ADS_GROUP_TYPE["UNIVERSAL"]
        if group_type & pyadconstants.ADS_GROUP_TYPE["GLOBAL"] == c_global:
            return "GLOBAL"
        elif group_type & pyadconstants.ADS_GROUP_TYPE["UNIVERSAL"] == c_universal:
            return "UNIVERSAL"
        else:
            return "LOCAL"

    def set_group_scope(self, new_scope: str) -> None:
        """
        Sets group scope

        :param new_scope: The new group scope.
        :type new_scope: str ["GLOBAL", "UNIVERSAL", "LOCAL"]
        :raises ValueError: If new_scope is not valid
        """

        if new_scope in ("LOCAL", "GLOBAL", "UNIVERSAL"):
            self.update_attribute(
                "groupType",
                (
                    self.get_attribute("groupType", False)
                    & pyadconstants.ADS_GROUP_TYPE["SECURITY_ENABLED"]
                )
                | pyadconstants.ADS_GROUP_TYPE[new_scope],
            )
        else:
            raise ValueError("new_scope", new_scope, ("LOCAL", "GLOBAL", "UNIVERSAL"))

    def get_group_type(self) -> str:
        """Returns group type DISTRIBUTION or SECURITY."""
        # 0x2, 0x4, 0x8 are the distribution group types since
        # a security group must include -0x80000000.
        if self.get_attribute("groupType", False) in (2, 4, 8):
            return "DISTRIBUTION"
        else:
            return "SECURITY"

    def set_group_type(self, new_type: str) -> None:
        """
        Sets group type

        :param new_type: the new group type
        :type new_type: str ['DISTRIBUTION', 'SECURITY']
        :raises ValueError: new_type is not a valid group type
        """

        if new_type == "DISTRIBUTION":
            self.update_attribute(
                "groupType",
                (
                    self.get_attribute("groupType", False)
                    ^ pyadconstants.ADS_GROUP_TYPE["SECURITY_ENABLED"]
                ),
            )
        elif new_type == "SECURITY":
            self.update_attribute(
                "groupType",
                (
                    self.get_attribute("groupType", False)
                    ^ pyadconstants.ADS_GROUP_TYPE["SECURITY_ENABLED"]
                )
                | pyadconstants._ADS_GROUP_TYPE["SECURITY_ENABLED"],
            )
        else:
            raise ValueError("new_type", new_type, ("DISTRIBUTION", "SECURITY"))


ADObject._py_ad_object_mappings["group"] = ADGroup


def __get_memberOfs(self, recursive: bool = False, scope: str = "all") -> List[ADGroup]:
    """
    Returns a list of groups that the current object is a member of.

    :param recursive: This determines whether to return groups that the object is nested
        into indirectly, defaults to False
    :type recursive: bool, optional
    :param scope: Only return group membership within the current domain (queries from
        domain) (scope=domain), the forest (will only include universal groups, queries
        from global catalog) (scope=forest), or both (scope=all), defaults to "all"
    :type scope: str ["domain","forest","all"], optional
    :return: ADGroup
    :rtype: List[ADGroup]
    """

    return self._get_memberOfs(recursive, scope, [])


def __is_member_of(self, group: ADGroup, recursive=False) -> bool:
    """
    Check whether this object is a member of the given group

    :param group: The group to check if an object is a member of
    :type group: ADGroup
    :param recursive: This determines whether to return groups that the object is nested
        into indirectly, defaults to False
    :type recursive: bool, optional
    :return: True if the object is a member of the group, False otherwise
    :rtype: bool
    """

    return group in self.get_memberOfs(recursive=recursive)


def ___p_get_memberOfs(
    self, recursive: bool = False, scope: str = "all", processed_groups: list = []
) -> List[ADGroup]:
    """
    Returns a list of groups that the current object is a member of.

    :param recursive: This determines whether to return groups that the object is nested
        into indirectly, defaults to False
    :type recursive: bool, optional
    :param scope: Only return group membership within the current domain (queries from
        domain) (scope=domain), the forest (will only include universal groups, queries
        from global catalog) (scope=forest), or both (scope=all), defaults to "all"
    :type scope: str ["domain","forest","all"], optional
    :param processed_groups: reserved, leave empty, defaults to []
    :type processed_groups: list, optional
    :return: ADGroup
    :rtype: List[ADGroup]
    """

    if self not in processed_groups:
        if scope in ("domain", "all"):
            for dn in self.get_attribute("memberOf"):
                obj = ADGroup.from_dn(dn, options=self._make_options())
                if recursive and obj not in processed_groups and dn != self.dn:
                    new_members = obj._get_memberOfs(recursive, scope, processed_groups)
                    processed_groups.extend(new_members)
                processed_groups.append(obj)
        if scope in ("forest", "all"):
            for dn in self.get_attribute("memberOf", source="GC"):
                obj = ADGroup.from_dn(dn)
                if recursive and obj not in processed_groups and dn != self.dn:
                    new_members = obj._get_memberOfs(recursive, scope, processed_groups)
                    processed_groups.extend(new_members)
                processed_groups.append(obj)
    return list(set(processed_groups))


ADObject.get_memberOfs = __get_memberOfs
ADObject.is_member_of = __is_member_of
ADObject._get_memberOfs = ___p_get_memberOfs