1. Customization
  2. Directives

Customization

Directives

For better flexibility the IdentityHub provides a set of schema directives that are ready for use in your extensions to achieve certain goals. Below are the available directives listed and described. They are chainable and will always work on the resolved value of the directive before.

@alias

Returns the value of another field. For example, as shown in the example below, you are now able to create a top level property on the User type that resolves to the value of any other property of that type, even nested ones.

graphql
type Manager {
  firstName: String!
  lastName: String!
}

extend type User {
  responsible: String! @alias(path: "manager.lastName")
  manager: Manager!
}

@buffer

Takes the parameter format, which can be one of the values of the enum BufferEncoding (base64, ascii, utf8, utf16le, ucs2, latin1, binary, hex). You can use this directive for example to create a string representation of the ldap entryUUID ()here in combination with the @ldapField directive.

graphql
extend type Role {
  entryUUID: String! @ldapField(attribute: "entryUUID", binary: true) @buffer(format: "hex")
}

You can create a string representation of the buffer value of any attribute. Of course it should be used for attributes that actually store binary data in LDAP. For example the user attribute publicKey. When retrieving binary attributes, it makes sense to set the binary flag of the @ldapField directive to true. By this, the IdentityHub will fetch the attributes value as buffer already and the '@buffer' directive does not have to transform it from a string to a buffer again. So with a schema definition like this:

graphql
extend type User {
  publicKey: String @ldapField(attribute: "publicKey", binary: true) @buffer
}

You could query like this:

graphql
query SearchUsers {
  users {
    nodes {
      # will return the publicKey value in the default format `base64` -> i.e.
      # AQAAAAQAAABMALAAAAAmAUwAZAB2AAAAAABPAFUAPQBJAGQAZQBuAHQAaQB0AHkALgBPAFUAPQBLAGUAbgBvAHgAYQAuAE8AVQA9AHUAcwBlAHIAcwAuAE8APQBkAGEAdABhAAAAQwBOAD0AdABoAHAAbABhAHQAaABlAC4ATwBVAxwfgenkAGUAbgB0AGkAdAB5AC4ATwBVAD0ASwBlAG4AbwB4AGEALgBPAFUAPQB1AHMAZQByAHMALgBPAD0AZABhAHQAYQAAAAEAAAADAAEAbABCVgQAMS4wAEJDAQADQkEBADBCTAIApAFOTjUA9eHeWJ1OMFu
      publicKey

      # will return the publicKey value encoded as utf8 -> i.e.
      # \u0001\u0000\u0000\u0000\u0004\u0000\u0000\u0000L\u0000�\u0000\u0000\u0000&\u0001L\u0000d\u0000v\u0000\u0000\u0000\u0000\u0000O\u0000U\u0000=\u0000I\u0000d\u0000e\u0000n\u0000t\u0000i\u0000t\u0000y\u0000.\u0000O\u0000U\u0000=\u0000I\u0000d\u0000e\u0000n\u0000t\u0000i\u0000t\u0000y\u0000.\u0000O\u0000U\u0000=\u0000K\u0000e\u0000n\u0000o\u0000x\u0000a\u0000.\u0000O\u0000U\u0000=\u0000u\u0000s\u0000e\u0000r\u0000s\u0000.\u0000O\u0000=\u0000d\u0000a\u0000t\u0000a\u0000\
      utf8: publicKey(format: utf8)

      # will return the publicKey value encoded as hex -> i.e.
      # 01000000040000004c00b000000026014c0064007600000000004f0055003d004900640065006e0074006900740079002e004f0055003d004b0065006e006f00780061002e004f0050079002e004f0055003d004b0065006e006f00780061002e004f0055003d00750073006500720073002e004f003d006400610074006100000001000000030001006c0042560400312e300042430100034241010030424c0200a4014e4e3500f5e1de589d4e305bbf7bac5a9e606a8ef58cd634b88b854c1e792e0b8c71a6b0bb76ec4cdea66380c454e03000100014d410800735ba6107f0b94776400505552534146
      hex: publicKey(format: hex)
    }
  }
}

This would be combinable for example with the @url directive, to retrieve the image as data url, ready to use in html.

@ensureArray

This directive takes care, that you will always receive an array when requesting a field. For example you can combine it with the @ldapField directive like so:

graphql
extend type User {
  firstNames: [String!]! @ldapField(attribute: "givenName") @ensureArray
}

Since we defined the field as array of strings, but we don't know if the ldap attribute givenName has a value or maybe just a single value, we now made sure, that our field will always return an array, even for a single or none value.

@ensureBoolean

This directive takes care, that you will always receive a correct boolean value (true or false) when requesting a field. This may be useful for certain types that are created via REST calls, i.e. to the userapp, and you cannot be sure if corresponding "Boolean" values will be correctly provided. This directives transforms the values true, 1, 'true' and 'TRUE' to the actual boolean value true and all other values (including null and undefined) to the actual boolean value false.

graphql
extend type UserappTask {
  isNewForm: Boolean! @ensureBoolean
}

@fieldset

With the fieldset directive you can gather a set of fields of a type to a single custom field. For example you already defined a set of custom attributes on the User type like so:

graphql
extend type User {
  booleanScalar: Boolean @ldapField(attribute: "passwordAllowChange")
  stringScalar: String @ldapField(attribute: "givenName")
  intScalar: Int @ldapField(attribute: "roomNumber")
  dateScalar: String @ldapField(attribute: "loginTime") @ldapTimestamp @formatDateTime(format: "yyyy-MM-dd'T'hh:mm:ss xxx")

  resolvedUser: User @ldapField(attribute: "directReports") @resolveDN
  resolvedUsers: [User!]! @ldapField(attribute: "directReports") @resolveDN

  resolvedGroup: Group @ldapField(attribute: "groupMembership") @resolveDN
  resolvedGroups: [Group!]! @ldapField(attribute: "groupMembership") @resolveDN
}

In your UI you may want to just query all custom attributes of a user. That's where the Fieldset comes in handy. Additionally to your already existing custom attributes, you could define something like:

graphql
extend type User {
  fieldset1: Fieldset @fieldset(fields: ["stringScalar", "booleanScalar", "intScalar", "dateScalar"])
  fieldset2: Fieldset @fieldset(fields: ["resolvedUser", "resolvedUsers", "resolvedGroup"])

  combinedFieldset: Fieldset @fieldset(fields: ["testpanel1", "testpanel2"])
}

The Directive takes a parameter fields, which is a list of field names of the type it is used in. Those fields must really exist and be either a Scalar type or implement the Labeled interface (see the graphql playground schema explorer for further insight on that interface and the Fieldset type). As you can see you can even collect other Fieldsets within a Fieldset.

A possible query may look like the following:

graphql
query GetUserFieldsets {
  userByDN(dn: "cn=example,o=data") {
    fieldset1 {
      label
      description
      fields {
        name
        description
        label
        type
        value
      }
    }
  }
}

As a response ypu would get:

graphql
{
  data: {
    userByDN: {
      fieldset1: {
        label: "A configured label translation for fieldset1",
        description: "A configured description translation for fieldset1"
        fields: [
          {
            name: "stringScalar",
            label: "A configured label translation for stringScalar",
            description: "A configured description translation for stringScalar"
            type: "String",
            value: "Max"
          },
          {
            "name": "booleanScalar",
            label: "A configured label translation for booleanScalar",
            description: null,
            type: "Boolean",
            value: true
          },
          {
            "name": "intScalar",
            label: "A configured label translation for intScalar",
            description: null,
            type: "Int",
            value: 103
          },
          {
            "name": "dateScalar",
            label: "A configured label translation for dateScalar",
            description: "A configured description translation for dateScalar"
            type: "String",
            value: "2022-04-26T12:20:11 +00:00"
          }
        ]
      }
    }
  }
}

For Fieldsets the fieldset namespace of our i18n library comes to use. With the translation files you can provide the labels/descriptions of a Fieldset and the corresponding Fields. These labels are determined using the name of the type that contains the Fieldset and the name of the Fieldset itself. If no language(s) are passed for the Fieldset in the Query, we use the languages from the Accept-Language header of the request. The fallbackLanguage described above is always added to the languages, if not already present.

Considering the example above, with the addition of passing the preferred languages:

graphql
query GetUserFieldsets {
  userByDN(dn: "cn=example,o=data") {
    fieldset1(locale: ["de", "fr", "es"]) {
      label
      fields {
        name
        label
        type
        value
      }
    }
  }
}

The possible translation keys for the label of the fieldset would be

  • <type>.<fieldset>.label (User.fieldset1.label)
  • <fieldset>.label (fieldset1.label)
  • <type>.<fieldset> (User.fieldset1)
  • <fieldset> (fieldset1)
  • If no translation is found, the fieldset name is used.

and for the description:

  • <type>.<fieldset>.descriptionUser.fieldset1.description
  • <fieldset>.descriptionfieldset1.description
  • If no translation is found, null is used

within the translation namespace fieldset, after that in the common namespace.

For the field translations, we use the same logic as above, with the following possible keys for the label of a field:

  • <type>.<fieldset>.<field>.labelUser.fieldset1.resolvedUser.label
  • <type>.<field>.labelUser.resolvedUser.label
  • <fieldset>.<field>.labelfieldset1.resolvedUser.label
  • <field>.labelresolvedUser.label
  • <type>.<fieldset>.<field>User.fieldset1.resolvedUser
  • <type>.<field>User.resolvedUser
  • <fieldset>.<field>fieldset1.resolvedUser
  • <field>resolvedUser
  • If no translation is found, the field name is used.

and for the description:

  • <type>.<fieldset>.<field>.descriptionUser.fieldset1.resolvedUser.description
  • <type>.<field>.descriptionUser.resolvedUser.description
  • <fieldset>.<field>.descriptionfieldset1.resolvedUser.description
  • <field>.descriptionresolvedUser.description
  • If no translation is found, null is used

@formatDateTime

It takes the parameters format and timeZone. It let's you transform a date into the desired format in the desired timeZone. For example you have a type Task, that resolves a timestamp for the created field:

graphql
type Task {
  created: @formatDateTime(format: "yyyy-MM-ddThh:mm" timeZone: "Europe/Berlin")
}

Defaults are format: yyyy-MM-dd'T'hh:mm:ssZ and timezone: UTC.

You can still override format and timezone in queries, so for the above schema example you could query i.e.:

graphql
query SearchTaskNodes {
  nodes {
    created(format: "yyyy-MM-dd" timezone: "America/New_York")
  }
}

For further insight on the available formats and timeZones please see https://www.npmjs.com/package/date-fns-tz

@id

Simplifies the usage of our internal ID mechanism (a base64 encoded string containing of <type>:<id>). Takes a parameter type, which, if not passed, will use the graphql type of the object containing the id property. For example with a type User you could write:

graphql
type User {
  id: ID! @ldapField(attribute: "entryUUID") @id
}

This will first retrieve the entryUUID attribute from LDAP, and will then generate a base64 encoded string from User:<entryUUID>. If you pass the type parameter, for example because a type Task contains its own id, as well as the id of the process it belongs to, you can ensure that the correct type is used:

graphql
type Task {
  id: ID! @id
  processId: ID! @id(type: "Process")
}

@ldapField

This directive can be used to map a field of a type to a certain ldap attribute and therefore resolve its value. It takes the parameters attribute, which is optional and if left out the name of the field itself will be used as attribute. Furthermore you can add a list of fallbackAttributes. If the ldap entry has no value for the original attribute, the fallbackAttributes will be used in the order they are declared. Within the ldap query all attributes (attribute and all fallbackAttributes will be fetched). The last parameter binary is a boolean that decides, wether the value will be fetched as binary value (i.e. for usage with the @buffer directive), or as a string representation. With the type of the field, you can define wether you want a single value or an array. Below you foind some examples on how to use this directive:

graphql
extend type User {
  # creates a resolver for the attribute 'objectClass' (alias is used as attribute) that returns an array of strings
  objectClass: [String!]! @ldapField

  # creates a resolver for the attribute 'givenName' that returns a string aliased under the attribute 'firstName'
  firstName: String @ldapField(attribute: "givenName")

  # creates a resolver for the attribute 'jpegPhoto' that returns a data uri with the content type 'image/jpeg'
  photo: URL @ldapField(attribute: "jpegPhoto") @url(contentType: "image/jpeg")

  # creates a resolver for the attribute 'date' that returns the ldap timestamp in the given format
  date: String @ldapField @ldapTimestamp @formatDateTime(format: "yyyy-MM-dd'T'hh:mm:ss xxx")
}

@ldapFind

This directive can be used to create a query resolver for the returned type and according to the passed parameters. The base parameter defines the ldap base DN for the search operation. It accepts either a real DN or a string in the form of attribute:<attributeName> which will use the value of the given ldap attribute of the root object as DN. With scope you can control the ldap search scope and filter let's you define additional custom ldap filters that will be combined with logical AND with other parameters like where, by or q that you may allow for that field. Additionally you can define a set of qAttributes (a list of attribute names) that will be matched with the value for the q parameter (equality). And with disablePaging you are able to turn off the ldap paged search and instead search with a size limit of 2. The ldap find will return any found entry as long it will find exactly one result. In case of no found entry or multiple entries matching the filter criteria, the resolver of this directive will retun null. With this example:

graphql
input ApplicationWhere {
  _and_: ApplicationWhere
  _or_: ApplicationWhere
  _not_: ApplicationWhere
  category: [StringMatchFilterInput!]
  cn: StringFilterInput
  applicationOwner: [StringMatchFilterInput!]
  applicationName: [StringMatchFilterInput!]
}

extend type Query {
  application(where: ApplicationWhere): Application @ldapFind(filter: "(&(objectClass=application)(appType=application))", scope: sub, base: "ou=applications,o=data")
}

you created a new query, that allows you to find a certain ldap entry of the examplary type Application. The filter must always match and is combined with the custom ApplicationWhere which can be passed at query time and creates a corresponding additional filter.

@ldapFindByAttribute

Can be used to create a base query resolver for the returned type and according to the passed parameters. The parameter attribute defines the attribute to match with the resolved value of the field, i.e.

graphql
extend type Role {
  application: Application
    @ldapField(attribute: "nrfRoleCategoryKey")
    @ldapFindByAttribute(base: "ou=applications,o=data", scope=sub, attribute: "appCategory", filter="(objectClass=application)")
}

This will first resolve the attribute nrfRoleCategoryKey of the Role via ldap and then find any Application that has this value as appCategory (exact match).

@ldapFindByDn

This directive can be used to create a resolver for a field that can resolve a complex type via ldap by its DN. There are multiple ways to use this directive with its several parameters. For example you may use the dn parameter:

graphql
extend type Query {
  applicationByDN(dn: DN!): Application @ldapFindByDN
}

This creates a new query applicationByDN (Application is an examplary ldap type) that you must pass a DN, which is then used to perform a ldap base search with that value and returns the entry if it is found.

It would be also possible to create a static query by passing a DN in the schema extension:

graphql
extend type User {
  manager: User @ldapFindByDn(dn: "cn=manager, ou=users,o=data")
}

In that case the manager field of a User will always resolve to the same manager for all users.

Additionally you can use the path parameter:

graphql
extend type User {
  managerDn: DN
  manager: User @ldapFindByDn(path: "managerDn")
}

This will take the managerDn field of the resolved User and will use that to resolve the manager field of the same User.

@ldapFindByEntryUuid

Can be used to create a query resolver to find an entry by its entryUUID attribute under the given base, the given scope and optionally an additional filter. For example:

graphql
extend type Query {
  applicationByEntryUUID(entryUUID: ID!): Application @ldapFindByEntryUuid(base: "ou=applications,o=data")
}

This creates a query applicationByEntryUUID that you must pass an ldap entryUUID and that will try to find an entry of type Application below the given base with the given entryUUID.

@ldapFindById

Works the same as the @ldapFindByEntryUuid directive with the only difference that not the ldap internal entryUUID is used, but the IdentityHub internal id that is a base64 representation of the type and the id of that type. (see the @id directive#id)

@ldapSearch

This directive let's you create a query resolver that performs a ldap search operation and resolves a Connection type based on the passed parameters base, scope, filter and qAttributes. The base parameter defines the ldap base DN for the search operation. It accepts either a real DN or a string in the form of attribute:<attributeName> which will use the value of the given ldap attribute of the root object as DN. The scope defines which ldap search scope is used and filter let's you define additional ldap filters that will be always joined with any other passed filters (via by, where or q query) with an AND operation. For example taken a custom type Application, you can define a query to search for those types:

graphql
input ApplicationWhere {
  _and_: ApplicationWhere
  _or_: ApplicationWhere
  _not_: ApplicationWhere
  category: [StringMatchFilterInput!]
  cn: StringFilterInput
  applicationOwner: [StringMatchFilterInput!]
  applicationName: [StringMatchFilterInput!]
}

extend type Query {
  applications(q: String, where: ApplicationWhere) @ldapSearch(base: "ou=applications,o=data", scope: sub, filter: "(objectClass=application)", qAttributes: ["applicationName", "applicationDescription"])
}

This query performs a ldap search operation with scope sub below the given base and will only find entries that match the objectClass application. Additionally we defined a q parameter for that query. By using it you can narrow down your results by the automatically generated additional ldap filter that will take the value of q and will match it against the given qAttributes. For a more complex use case we also defined the ApplicationWhere which can provide a more flexible way of creating filters (see the schema docs in the playground for more details).

@ldapTimestamp

Transforms the ldap timestamp to ISO conform timestamps. For formatting you can combine this directive with the @formatDateTime directive:

graphql
type User {
  lastLoginDate: String @ldapField(attribute: "loginTime") @ldapTimestamp @formatDateTime(format: "yyyy-MM-dd" timeZone: "Europe/Berlin")
}

@localizeLdapAttribute

Use this directive for ldap multi language attributes to extract the language you need. Takes the parameters localize (can be set to false in a query if you need the whole string for some reason) and locale (define the preferred language to extract). The locale will, if not passed as parameter use the requests accept-language header to determine your browser language and will fall back to the configured i18n.fallbackLanguage if nothing else is found. Given the example value of the attribute nrfLocalizedNames of a role as de~deutscher Name|en~english name and a schema definition like below

graphql
type Role {
  name: @ldapField(attribute: "nrfLocalizedNames") @localizeLdapAttribute
}

you could query like this (assume that the language from the accept-language header will result in de):

graphql
query SearchRoles {
  roles {
    nodes {
      name # returns 'deutscher name'
      englishName: name(locale: "en") # returns 'english name'
      italianName: name(locale: "it") # returns 'english name' (fallback)
      unlocalizedName: name(localize: false) # returns 'de~deutscher Name|en~english name'
    }
  }
}

@resolveDn

Can be used to create a query resolver to find an entry by its DN using a ldap base search. The parameter path defines which property of the parent object should be resolved. This direcxtive also can handle multi value fields. You decide wether you want to receive a single resolved type or an array in your schema extension. For example you can extend a User:

graphql
extend type User {
  resolvedGroup: Group @ldapField(attribute: "groupMembership") @resolveDN
  resolvedGroups: [Group!]! @ldapField(attribute: "groupMembership") @resolveDN
}

Both queries will use the resolved value of the @ldapField directive and will start to resolve the DNs that are saved in the multi value attribute groupMembership. The first field resolvedGroup will only resolve the first DN found in groupMembership, while the second one will resolve all of them in an array.

Alternatively you can use the additional path parameter. If the @resolveDN directive comes first and there is nor former resolved value, we will use the value of the attribute defined by path. For example:

graphql
extend type User {
  manager: DN
  resolvedManager: User @resolveDN(path: "manager")
}

will take the DN from the Users manager field and resolve that for the resolvedManager field.

@resolveDnConnection

Can be used to create a query resolver to find a entries by their DN using a ldap base search and returns them as connection. The parameter path defines which property of the parent object should be resolved (if there is not alredy another resolver before @resolveDnConnection in which case the resolved value would be used). The resolvedType parameter can be set if you are sure of what type the results will be. In case the returned type will be i.e. a union, leave this parameter out. It will get inferred by objectClass queries for every node. Because this will return a Connection, you should take care that the query results are pageable:

graphql
extend type User {
  groupMembership: [DN!]! @ldapField

  resolvedGroups1(paging: LDAPPaging): GroupConnection! @resolveDNConnection(path: "groupMembership", resolvedType: "Group")
  resolvedGroups1(paging: LDAPPaging): GroupConnection! @ldapField(attribute: "groupMembership") @resolveDNConnection(resolvedType: "Group")
}

The fields resolvedGroups1 and resolvedGroups2 in the example above, will do exactly the same. The first one uses the path parameter to use the value from the groupMembership field, the second one takes care to resolve that value on its own by using the @ldapField directive. Since we already know that we will resolve a connection of Groups, we can pass that type via the resolvedType parameter. This will improve the performance of the query since we do not need to infer the type via seperate ldap queries:

@url

Takes the parameters contentType and encoding. Can be used i.e. to create a data url for a user avatar image that can be used directly in the src attribute of an image tag in the browser:

graphql
type User {
  photo: URL @ldapField(attribute: "jpegPhoto", binary: true) @url(contentType: "image/jpeg")
  homepage: URL @ldapField(attribute: "homepage", binary: true) @url(contentType: "text/html")
}