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:
- msad TOML file (optional) —
~/.msad.tomlby default, or the path inMSAD_CONFIG_FILE. TheMSAD_DOMAINvariable selects which[domains.<name>]section to use (otherwise the[defaults] domain). 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_PASSWORDand leave them unset in the TOML → Kerberos/SASL bind (runkinitfirst). - Set both
MSAD_USERandMSAD_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 byuser,users,inactiveUsers,groupMembers,passwordPolicyViolations,neverExpiringAccounts,expiredAccounts,passwordNotRequired,kerberosPreauthNotRequiredandnotDelegated.Group— returned bygroup,groups,emptyGroups,securityGroups,userGroupsandprivilegedGroups.Computer— returned bycomputerandcomputers.Ou— returned byouandous.
All of them carry:
raw— the complete, dynamic LDAP entry as aJSONscalar (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 themanagerDN into a fullUser.User.memberOfGroups: [Group!]!— resolves thememberOfDNs intoGroupobjects.User.directReportsUsers: [User!]!— resolves thedirectReportsDNs intoUserobjects.Group.managedByUser: User— resolves themanagedByDN into the owningUser.Group.memberUsers: [User!]!— resolves thememberDNs intoUserobjects (nested groups/computers among the members are skipped; usegroupMembersfor msad's person-oriented listing).Group.members: [GroupMember!]!— resolves everymemberDN to its concrete type (see the union below), keeping nested groups and computers.Group.memberGroups: [Group!]!— the nested groups directly inmember(members whose objectClass isgroup; 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'smanagedByDN into the owningUser.Ou.managedByUser: User— resolves the OU'smanagedByDN into the owningUser.
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 singleload_fninvocation.
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(optionallyexcludeDomainControllers: true),staleComputers,domainControllers,osSummary,computer. - Groups (→
Group) —groups,emptyGroups,securityGroups(optionallyscope: "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/Ouobjects (withrawexposing the full entry); the other queries (e.g.contacts,ouContents, audit reads) expose entries directly as an openJSONscalar inside typed envelopes (count+entries, orentry). 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| graphqlad-0.1.1.tar.gz | 114.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.