1. Graphql
  2. LDAP

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:

toml
[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

graphql
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.

graphql
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.

graphql
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.

TIP

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.

WARNING

Be aware that only one attribute is allowed in these queries.

A query with that mechanism could look like that:

graphql
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 Roles, Groups, and Containers 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.

TIP

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.

WARNING

Please keep in mind, that this will expose the whole LDAP tree.

toml
[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.

toml
[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.

graphql
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.

graphql
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.