Skip to main content

graphqlAD

A read-only GraphQL interface over the msad Active Directory library, built with Strawberry + FastAPI, managed with uv on Python 3.14.

Only LDAP search operations are exposed. The schema has a single Query type and no Mutation type, so no write operation (create / modify / delete / password reset / group membership changes) is reachable through the API.

It is a GraphQL sibling of restAD (REST) and mcpAD (MCP): all three are thin, read-only front ends that call the same typed msad API directly.

How configuration is resolved

The effective connection config (an msad.DomainConfig) is built in two layers, with the environment always winning:

  1. msad TOML file (optional) — ~/.msad.toml by default, or the path in MSAD_CONFIG_FILE. The MSAD_DOMAIN variable selects which [domains.<name>] section to use (otherwise the [defaults] domain).
  2. MSAD_* environment overrides — applied on top of the TOML values.

This lets you keep non-secret settings in the TOML file (or omit it entirely) and inject credentials and any connection parameter at runtime via the environment, so secrets never need to live on disk.

Environment variables

Variable Overrides / effect
MSAD_CONFIG_FILE Path to the msad TOML file (default ~/.msad.toml).
MSAD_DOMAIN Which [domains.<name>] section to use.
MSAD_HOST Directory host (e.g. dc.example.com).
MSAD_BASE Search base DN (e.g. dc=example,dc=com).
MSAD_PORT Port (int, e.g. 636).
MSAD_USE_SSL LDAPS on/off (true/false/1/0/yes/no).
MSAD_USER Bind user. Setting user and password enables simple bind.
MSAD_PASSWORD Bind password.
MSAD_QUERY_TIMEOUT Per-operation receive timeout in seconds (int).

host and base are required (from either the TOML file or the environment).

Authentication:

  • Omit MSAD_USER/MSAD_PASSWORD and leave them unset in the TOML → Kerberos/SASL bind (run kinit first).
  • Set both MSAD_USER and MSAD_PASSWORD → simple user/password bind.

Server variables

Variable Effect
GRAPHQLAD_HOST HTTP bind host for the graphqlad command (default 127.0.0.1).
GRAPHQLAD_PORT HTTP bind port (default 8000).
GRAPHQLAD_LAZY_CONNECT If true, start even if the directory is unreachable at boot.
GRAPHQLAD_GRAPHIQL Serve the in-browser GraphiQL IDE at /graphql (default true). Set to false to disable it.

Install

uv sync

The msad dependency is pulled from PyPI (msad>=0.9). Kerberos (SASL/GSSAPI) authentication is optional: msad's gssapi dependency is a native extension that needs the system Kerberos dev headers to build (e.g. libkrb5-dev + krb5-config on Debian/Ubuntu, krb5-devel on RHEL/Fedora, krb5 via Homebrew on macOS). If you only authenticate with user/password, skip it entirely; to enable Kerberos, sync the kerberos extra instead:

uv sync --extra kerberos
# or: pip install 'graphqlad[kerberos]'

Common developer tasks run through the Makefile:

make sync     # install/refresh the dev environment
make lint     # ruff check + format check
make test     # run the test suite
make build    # build wheel + sdist into dist/
make publish  # upload to PyPI (needs credentials)

Run

Credentials are passed via the environment:

export MSAD_HOST=dc.example.com
export MSAD_BASE='dc=example,dc=com'
export MSAD_PORT=636
export MSAD_USE_SSL=true
export MSAD_USER='svc_reader'
export MSAD_PASSWORD='s3cr3t'

uv run graphqlad
# or:
uv run uvicorn graphqlad.app:app --host 0.0.0.0 --port 8000

Then open the GraphiQL IDE at http://127.0.0.1:8000/graphql (the same URL serves GraphQL POST queries).

Run with uvx

uvx runs graphqlAD in a throwaway, isolated environment — no clone or manual install needed. The connection is still configured entirely through the environment.

# From PyPI (once graphqlAD is published):
MSAD_HOST=dc.example.com MSAD_BASE='dc=example,dc=com' \
MSAD_USER=svc_reader MSAD_PASSWORD='s3cr3t' \
  uvx graphqlad

# From a local checkout:
uvx --from . graphqlad

# Straight from the Git repository (no clone):
uvx --from git+https://github.com/matteoredaelli/graphqlAD graphqlad

All MSAD_* and GRAPHQLAD_* variables work exactly as above. Because uvx builds msad (and its gssapi C extension) in a fresh environment, the krb5 development headers must be available on the host (see Install).

The schema

A single Query type exposes every read operation. Example query:

{
  health { ok target auth error }

  users(surname: "Rossi", department: "IT", limit: 10) {
    count
    users {
      samAccountName
      displayName
      raw                  # the full dynamic LDAP entry, as a JSON scalar
      managerUser {        # VIRTUAL: `manager` DN expanded into a User
        displayName
        managerUser { displayName }   # ...recursively, if you want
      }
    }
  }

  user(identifier: "mrossi") {
    displayName
    raw
    managerUser { displayName mail }
  }

  userStatus(identifier: "mrossi") {
    isDisabled
    isLocked
    hasExpiredPassword
    hasNeverExpiresPassword
  }

  osSummary { count items { operatingSystem count } }
}

Typed User / Group / Computer / Ou objects and virtual fields

Queries that return users, groups, computers or OUs expose typed objects rather than bare JSON entries:

  • User — returned by user, users, inactiveUsers, groupMembers, passwordPolicyViolations, neverExpiringAccounts, expiredAccounts, passwordNotRequired, kerberosPreauthNotRequired and notDelegated.
  • Group — returned by group, groups, emptyGroups, securityGroups, userGroups and privilegedGroups.
  • Computer — returned by computer and computers.
  • Ou — returned by ou and ous.

All of them carry:

  • raw — the complete, dynamic LDAP entry as a JSON scalar (nothing is lost; every attribute the directory returned is here).
  • a few convenience identity scalars read from raw (User: dn, samAccountName, displayName, mail; Group: dn, samAccountName, cn, displayName; Computer: dn, samAccountName, cn, dnsHostName, operatingSystem; Ou: dn, ou, description).
  • virtual fields — derived fields that are not raw LDAP attributes. Naming convention: a DN-valued attribute expanded into object(s) gets a suffix naming the resolved type. Currently:
    • User.managerUser: User — resolves the manager DN into a full User.
    • User.memberOfGroups: [Group!]! — resolves the memberOf DNs into Group objects.
    • User.directReportsUsers: [User!]! — resolves the directReports DNs into User objects.
    • Group.managedByUser: User — resolves the managedBy DN into the owning User.
    • Group.memberUsers: [User!]! — resolves the member DNs into User objects (nested groups/computers among the members are skipped; use groupMembers for msad's person-oriented listing).
    • Group.members: [GroupMember!]! — resolves every member DN to its concrete type (see the union below), keeping nested groups and computers.
    • Group.memberGroups: [Group!]! — the nested groups directly in member (members whose objectClass is group; users/computers excluded).
    • Group.memberOfGroups: [Group!]! — the groups this group is itself a member of (memberOf, i.e. group nesting upward).
    • Computer.managedByUser: User — resolves the computer's managedBy DN into the owning User.
    • Ou.managedByUser: User — resolves the OU's managedBy DN into the owning User.

Computer is also used as a possible member type in the GroupMember union below.

The GroupMember union

A group member can be a user, a nested group, or a computer. Group.members returns a GraphQL union GroupMember = User | Group | Computer. The concrete type of each member is determined from its objectClass (computer is checked before user, since AD computer accounts carry both). Members of any other object class, and unresolvable DNs, are skipped.

Select per-type fields with inline fragments:

{
  group(identifier: "all-staff") {
    members {
      __typename
      ... on User { samAccountName displayName }
      ... on Group { cn }
      ... on Computer { dnsHostName operatingSystem }
    }
  }
}

Virtual fields are resolved lazily: the extra LDAP lookup (get_by_dn) only happens when you actually select the field. Selecting just raw or the scalar fields performs no additional query. The raw DNs are always still available (raw.manager, raw.memberOf, raw.directReports, raw.managedBy, raw.member). Unresolvable DNs are skipped in list fields.

Note: ouContents (listing arbitrary objects under an OU) and contacts still return the generic Entries/Entry JSON envelope, since their results mix object types or have no DN-valued attributes worth expanding.

Because groupMembers returns User objects, you can expand each member's managerUser directly; and since the virtual fields are themselves typed, you can chain them (e.g. user { managerUser { managerUser { displayName } } }).

DN resolution and the DataLoader

Virtual fields resolve DNs through a per-request DataLoader (one instance per GraphQL request, created lazily on first use). It provides:

  • Within-request caching (deduplication): the same DN is resolved only once per request, even if several fields reference it (e.g. a manager who is also listed under member/directReports). It caches what was loaded on demand — it does not pre-fetch anything you did not ask for.
  • Batching: concurrent managerUser / memberUsers / … resolutions in the same event-loop tick are grouped into a single load_fn invocation.

The cache lives only for the request, so results never go stale between requests. Since msad.get_by_dn resolves one DN at a time (the DN is used as the search base), the loader issues one lookup per distinct DN; it does not collapse several DNs into a single LDAP query. The main win is deduplication and avoiding repeated lookups of the same DN, plus not blocking the event loop (the synchronous msad calls run in a worker thread).

Available queries

  • health
  • Users (→ User) — users, inactiveUsers, user, userStatus, neverExpiringAccounts, expiredAccounts, passwordNotRequired, kerberosPreauthNotRequired, notDelegated (risk checks).
  • Computers (→ Computer) — computers (optionally excludeDomainControllers: true), staleComputers, domainControllers, osSummary, computer.
  • Groups (→ Group) — groups, emptyGroups, securityGroups (optionally scope: "global" | "domain_local" | "universal"), group, userGroups, groupMembers (→ User), isMember.
  • Contacts — contacts.
  • OUs (→ Ou) — ous, ou; ouContents (mixed object types).
  • Audit — domainInfo, passwordPolicy, passwordPolicyViolations (→ User), privilegedGroups (→ Group), fsmoRoles, trusts, sites, gpos, gpo, fineGrainedPasswordPolicies.

Notes

  • LDAP entries are dynamic (attributes depend on what the directory returns). User-, group-, computer- and OU-returning queries wrap them in the typed User / Group / Computer / Ou objects (with raw exposing the full entry); the other queries (e.g. contacts, ouContents, audit reads) expose entries directly as an open JSON scalar inside typed envelopes (count + entries, or entry). Non-JSON-native values (datetimes, bytes) are coerced to strings for transport.
  • A single LDAP connection is opened during the app lifespan and shared across requests; it is unbound at shutdown.

License

GPL-3.0-or-later. Copyright © Matteo Redaelli.

Metadata

Release files for graphqlad 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for graphqlad 0.1.1
File Size Uploaded Provenance
graphqlad-0.1.1.tar.gz 114.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for graphqlad 0.1.1
File Interpreter ABI Platform
graphqlad-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 147.7 kB

Release files / graphqlad-0.1.1.tar.gz

Download URL graphqlad-0.1.1.tar.gz
Size 114.5 kB
Tags Source
SHA-256 checksum
How to use checksums
4d8f5dd41e3565630128a128f2fe7393ab4877de7913020b3ee1b9a95152438d
BLAKE2b-256 checksum
How to use checksums
03cfd37f18c63a9b08f5a968c10b62e8385e71925e44753093722a7d95fa56ef
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / graphqlad-0.1.1-py3-none-any.whl

Download URL graphqlad-0.1.1-py3-none-any.whl
Size 33.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a6f569080931b52dd75077fcb9eee2a04ef2e23f7effb88f762be0e3a984fd73
BLAKE2b-256 checksum
How to use checksums
c82bd7fc6220c1cad9bf2e32de39fee9ff6fb4f30ce82cb6d1a4d623d3b26455
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for graphqlad 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page