Graphql
LDAP
This document provides an overview about types and queries concerning the LDAP directory.
LDAP profiles
The IdentityHub is able to either connect an eDirectory or an Active Directory. The default is eDirectory. You can switch the profile via the profile
property of the ldap
config section:
[ldap]
profile="Active Directory"
Keep in mind, that if you are using Active Directory all Userapp parts of the schema are not included.
LDAP types
The IdentityHub provides special types and queries for 4 different LDAP types.
For each of those types (User
, Role
, Group
and Container
and even the generic LDAPSearchResult
) you can configure a certain base
, scope
and filter
that are taken into account for every search operation.
Furthermore you can configure which LDAP attributes to resolve each graphql field.
Another property you can set for every type are the qAttributes
which will be explained later.
Below you see an example configuration for the User
type. For all other types the configuration works exactly the same:
[ldap]
[ldap.User]
base="ou=users,o=data"
scope="sub"
filter="(objectClass=Person)"
qAttributes=["cn", "givenName", "sn", "mail"]
[ldap.User.fields]
id="guid"
name="cn"
label=["customLabelAttribute", "fullName", "cn"]
The ldap.xxx.fields
let's you configure for each field of the User
type which LDAP attributes to use to resolve it. You can configure a single string or an array of strings. When configuring an array, the IdentityHub will resolve the first LDAp attribute that has a value.
LDAP type based queries
Each of the four LDAP types (User
, Role
, Group
and Container
) provides a subset of queries.
For every type, there is a find query that will return null or a single result (if only exactly one result matches the filter for that type). For the User
type this query is named user
, for the Role
type role
, etc.
There are also search queries for every type named users
, roles
, groups
, containers
. All of those will return a Connection (UserConnection
, RoleConnection
, GroupConnection
and ContainerConnection
).
The find and search queries always take the same arguments (q
, by
and where
), which are used for filtering. The search queries take the additional parameters paging
and sort
which are used for pagination and sorting.
Filtering LDAP types
There are three possibilities to filter the described LDAP types. You can use the q
parameter, the by
parameter or the where
parameter. Those parameters can also be combined if desired. Everything you provide by these parameters in combination with the configurable filter
for a certain type will be transformed to a LDAP filter and then used to query against the directory.
q
Use the q
parameter as a shortcut for simple queries. It utilizes the configured qAttributes
for the given type and creates an OR
combined contains
filter for all the configured attributes. For example, if the attributes cn
and mail
are configured as qAttributes
for the User
type, a query like
query SearchUsersWithQ {
users(q: "test") {
nodes {
id
name
}
}
}
will create the LDAP filter
(|(cn=*test*)(mail=*test*))
and combine it with the configured filter
for the User
type (ldap.User.filter
) which results in the final filter
(&(objectClass=Person)(|(cn=*test*)(mail=*test*)))
The query will return all users that are matching this filter and can be found under the configured users base
with the configured users scope
.
This works as well for the single queries and the connection queries.
by
With the by
filter, you can create a combination of LDAP equals
filters for a defined set of properties. To see what attributes are allowed for filtering, try them out in the playground.
In the following example for the by
filter we try to find all users that have a firstName of either Max
or Maria
and a lastName Mustermann
.
query SearchUsersWithBy {
users(
by: {
firstName: ["Max", "Maria"],
lastName: "Mustermann"
}) {
nodes {
id
name
}
}
}
This will create the LDAP filter, this time with strict equals
and combined with AND
and OR
for the array for firstName
.
(&(|(givenName=Max)(givenName=Maria))(sn=Mustermann))
but also combined with the configured filter
for the User
type which results in the final filter
(&(objectClass=Person)(|(givenName=Max)(givenName=Maria))(sn=Mustermann))
This works as well for the single queries and the connection queries.
where
The where
filter is the most complex filter you can use. It allows various combinations of and
, or
and not
filters. The filter parts themself are not restricted to equals
but also allow things like contains
, greater than
, lower than
, excludes
, depending on the attribute and what filter options are defined for that attribute in the schema. Again the best way to find out about the possibilities is to use the playground and experiment with the possible combinations.
In the following example we try to find all users, that have been created after a certain date, that have a lastName that starts with an M
but not end with a a
.
query SearchUsersWithWhere {
users(
where: {
createTimestamp: { gt: "2021-07-22" }
lastName: { startsWith: "M" }
_not_: { lastName: { endsWith: "a" } }
}
) {
nodes {
id
name
}
}
}
The LDAP filter that is created by that is
(&(createTimestamp>=20210722000000Z)(!(createTimestamp=20210722000000Z))(sn=M*)(!(sn=*a)))
and also combined with the configured filter
for the User
type which results in the final filter
(&(objectClass=Person)(createTimestamp>=20210722000000Z)(!(createTimestamp=20210722000000Z))(sn=M*)(!(sn=*a)))
This works as well for the single queries and the connection queries.
You may want to extend the UserBy
and UserWhere
input via
schema extension if you need to filter by other or a custom attribute. Of course the same goes for the other ldap types and their filter inputs.
additional *By queries
The four defined types provide another way to query. Those are named usersBy
, rolesBy
, groupsBy
and containersBy
. The purpose of these queries is to perform multiple find operations for multiple inputs and return the results in the exact same order as the input parameters. For every type there is a *By
input type that defines which attributes can be filtered. For the User
type it looks like the following:
"""
Used as filter for the \`usersBy\` query
"""
input UsersBy {
"""
Filters \`User\`s by the configured users \`id\` attribute
"""
ids: [ID]
"""
Filters \`User\`s by the \`dn\` attribute
"""
dns: [DN]
"""
Filters \`User\`s by the configured \`name\` attribute or the defined name attributes for a \`User\`
"""
names: [String]
"""
Filters \`User\`s by the \`entryUUID\` attribute
"""
entryUUIDs: [String]
}
Feel free to extend this input via schema extension if you need to filter by a custom attribute.
Be aware that only one attribute is allowed in these queries.
A query with that mechanism could look like that:
query SearchUsersByBy {
usersBy(
names: ["mmustermann", "amuster", "bmann"]
) {
dn
firstName
lastName
}
}
As seen in the explanation of the UsersBy
input type, the names parameter will filter either by the name attributes given by the schema or the configured nameAttributes
for users. For example the nameAttributes
is "cn"
, this query will execute three single queries with the ldap filters (again combined with the configured users filter
under the configured users base
and scope
):
(&(objectClass=Person)(cn=mmustermann))
(&(objectClass=Person)(cn=amuster))
(&(objectClass=Person)(cn=bmann))
given that only the first and last users with the corresponding cn exist, you would get the following result:
data: {
usersBy: [
{
dn: "cn=mmustermann, ou=users,o=data",
firstName: "Max",
lastName: "Mustermann"
},
null,
{
dn: "cn=bmann, ou=users,o=data",
firstName: "Bert",
lastName: "Mann"
}
]
}
Conclusion
As you can see, there are a lot of possibilities and combinations to create any filter you want. Also the q
, by
, and where
filters can be combined. Even though the examples only covered the User
type, they work exactly the same for Role
s, Group
s, and Container
s with the only difference that the allowed attributes to filter by differ from type to type. Nevertheless you have the possibility to enhance the filter capabilities with a schema extension. This would allow you to i.e. add a filter for a custom attribute that is not already included in the LDAP types the IdentityHub provides.
For more insight on schema extensions please have a look at this section.
LDAP generic queries
Additionally to the defined LDAP types, you have the possibility to query any object in the directory and any attribute of that object independet from its type. This possibility is disabled by default in production. You can still enable it via config if there is a need for it.
Please keep in mind, that this will expose the whole LDAP tree.
[ldap]
enableGenericGraphQLQueries=true
As well as for the type defined queries there is a generic one to find exactly one result, named find
, and one that returns a connection, named search
. The returned objects are of the type LDAPSearchResult
.
Those queries are using the configured default base
and scope
, but you can pass the parameters base
and scope
for every single query. The default base
and scope
are configured in the ldap section of the configuration.
[ldap]
base="o=data"
scope=sub
The filter and paging possibilities are the same for the generic queries as for the typed queries with the difference that you can pass the qAttributes
as an additional parameter (defaults to cn
). To enhance the filter possibilities for by
and where
you may need to extend the input types LDAPEntryBy
and LDAPEntryWhere
(see
schema extensions).
The type LDAPSearchResult
has some basic default fields you can request without further ado. Those fields are id
, dn
, entryUUID
, objectClasses
, createTimestamp
and modifyTimestamp
.
The following query shows a generic search with a simple q
filter with custom qAttributes
, base
and scope
. The filtering will not be explained since they work the same way as for the typed queries.
query SearchGeneric {
search(
q: "test",
qAttributes: ["cn", "ou"],
base="ou=test, o=data",
scope=one
) {
nodes {
id
dn
entryUUID
objectClasses
createTimestamp
modifyTimestamp
}
}
}
To query other any arbitrary attribute, you can make use of the special fields of the LDAPSearchResult. Those are string
, strings
, number
, numbers
, boolean
, booleans
, buffer
, buffers
, date
, dates
, resolveDN
and resolveDNS
. The fields ending with the letter s
are used for multi value attributes and alwas return an array of values (even if the attribute has no or only one value). Most of those fields should be self explanatory. For example the string
field is used to get the value of a LDAP string attribute.
All of those fields can take the parameters attribute
(which defines the attribute to fetch from the directory), fallbackAttributes
(an array of attribute names to use if the original attribute has no value) and the boolean binary
, to decide wether to fetch the attribute's value as binary representation.
The buffer
and buffers
fields add an additional parameter encoding
to decide in which format the string representation of the binary data is encoded.
The resolveDNs
field additionally has a parameter paging
to enable paging for the received array of results. This field may for example be used to resolve the groups of a user. Those may be a lot of values in some environments and since the IdentityHub has to resolve every single DN we decided to return a pageable result for that field.
The generic attributes have to be used with aliases.
Example usage of the generic fields is shown in the query below.
query SearchGeneric {
search(
q: "test",
qAttributes: ["cn", "ou"],
base="ou=test, o=data",
scope=one
) {
nodes {
# returns the fullName attribute value if existing, otherwise the sn attribute or the givenName attribute
name: string(attribute: "fullName", fallbackAttributes: ["sn", "givenName"])
# returns an array with all values of the multi value attribute mail
emails: strings(attribute: "mail")
# returns a single boolean that will be `false` if the attribute does not exist for the entry
passwordChangePossible: boolean(attribute: "passwordAllowChange")
# returns a number representation of the value for the attribute `revision`
number(attribute: "revision")
# returns the binary data for the attribute `jpegPhoto` as a string representation endoded in `base64` format
avatar: buffer(attribute: "jpegPhoto", format: "base64")
# returns the first 3 groups resolved by the DNs stored in the attribute `groupMembership` and resolves the attributes dn and objectClasses of those groups
groups: resolveDNs(attribute: "groupMembership", paging: {first: 3}) {
nodes {
id
objectClasses
}
}
}
}
}
LDAP/NMAS password check
Since version v9.6.0 there is passwordCheck
field on a User
as well as on the Viewer
. It takes a password string and checks it against the NMAS password policy of the eDirectory without setting the password for the user.
Usage:
query passwordChecks {
user(by: "...") {
checkPassword(password: "newPassword") {
status
code
label
description
}
}
viewer {
checkPassword(password: "123") {
status
code
label
description
}
}
}
The result may look like this:
{
"data": {
"user": {
"checkPassword": {
"status": "PASSWORD_NUMERIC_MIN",
"code": -16008,
"label": "Not enough numeric characters",
"description": "The password does not contain the minimum number of numeric characters required by the password policy."
}
},
"viewer": {
"checkPassword": {
"status": "PASSWORD_TOO_SHORT",
"code": -216,
"label": "Password too short",
"description": "The password is too short."
}
}
}
}
The checkPassword
resolver takes another argument locale
(string or array of strings) which can be used to set the preferred languages for the translation of the label
und description
fields of the result. If not passed, the browser languages are used.
We already provide translations for label and description in english, german, spanish and french. If you want to add languages or adapt certain values, create a file check-password.yaml
in the corresponding folder (For example locales/de
for german). See the documentation on configuring translation files.