---
title: Enumerate AD group membership
summary: Expand direct or nested AD membership into a distinct account list.
status: draft
owner: SAIF Framework
last_reviewed: 2026-07-30
library: SAIF.Framework.Security.EnvironmentApi
reference_type: package
operation_id: OP-ENV-008
trigger: MachineSecurityService or downstream callers request group members
---

# Enumerate AD group membership

This operation is the library's local LDAP traversal workflow and the data source that machine authorization ultimately depends on. It resolves the target AD group, optionally walks nested subgroups, accumulates distinct user accounts while guarding against cycles and duplicates, and returns the sorted `sAMAccountName` list that authorization logic compares against.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-ENV-008 |
| **Trigger** | Caller requests the users contained in an AD group |
| **Inputs** | `groupName`, `ldap`, `domain`, `recurse` |
| **Outputs** | Distinct sorted `string[]` of `sAMAccountName` values, or `null` |
| **Primary components** | `ADService.GetUsersByGroup`, `ADService.GetMembership`, `ADService.GetMembershipRecursively` |
| **Entry point** | `SAIF.Framework.Security.EnvironmentApi\Core\Services\ADService.cs` |

## Steps

1. Open a `PrincipalContext` for the requested domain and resolve the target group with `GroupPrincipal.FindByIdentity`.
2. If the group cannot be found, return `null` without throwing, so a caller can distinguish "group does not exist" from "group has no members."
3. Open a `DirectoryEntry` rooted at the supplied LDAP path and begin traversal from the group's distinguished name.
4. Query direct membership with a `DirectorySearcher` filter of `(&(objectClass=<type>)(memberOf=<groupDn>))`, loading `objectGUID`, `sAMAccountName`, and `distinguishedName`, and sorting the results by `sAMAccountName`.
5. When `recurse` is `true`, visit subgroup results first, skipping any subgroup name that has already been traversed so nested membership cycles cannot cause infinite or duplicated work.
6. Visit user results, skipping any user name that has already been yielded so the same account does not appear twice when it belongs to multiple groups in the traversal path.
7. Project the collected results to `sAMAccountName`, apply `Distinct()` and `OrderBy()`, and return the final array that authorization checks consume ([BR-006](../business-rules.md#br-006-machineservice-account-authorization-via-ad-group-membership)).

## Process flow

The following diagram visualizes this operation's control flow.

```mermaid
flowchart TD
	Start([Caller requests AD group members]) --> Group[Resolve group identity]
	Group --> Exists{Group found?}
	Exists -->|No| ReturnNull([Return null])
	Exists -->|Yes| Root[Open LDAP root]
	Root --> Nested{Recurse into subgroups?}
	Nested -->|Yes| Walk[Walk unseen subgroups]
	Nested -->|No| Users[Read direct users]
	Walk --> Users
	Users --> Distinct[Deduplicate and sort sAMAccountName values]
	Distinct --> End([Return member array])
```

## Failure and edge-case behavior

- A missing group returns `null` rather than an empty array, so callers can tell "no such group" apart from "group exists but is empty."
- Recursion tracks seen subgroup and user names in separate sets to prevent both infinite cycles and duplicate output.
- The method does not catch LDAP or directory-service exceptions; those propagate to the caller unhandled.

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)
- [Security and authorization](../security-authorization.md)

## Sources
- SAIF.Framework.Security.EnvironmentApi\Core\Services\ADService.cs
