1. Customization
  2. Schema extensions

Customization

Schema extensions

With schema extensions you can create new or extend existing GraphQL types or create new queries.

Create a schema extension

Whilst the find and search queries already allow a high degree of freedom because with them you are able to access any attribute of an LDAP entry you want, they should only be used for development and test environments. By extending the schema in a well-defined way, you can enhance your possibilities in a safe manner.

You can extend the GraphQL schema via the IdentityHub config. Under the graphql section in the config, you may provide the schemaExtension option.

The value can either be configured as plain string or as a path to a file. The path is resolved relative to the config directory.

If you want to use a path, configure it as follows:

local.toml
[graphql]
  schemaExtension="file://path/to/extension/file.graphql"
TIP

We recommend to use a separate file. This is way easier to read and your editor can provide you with correct syntax highlighting for graphql files.

Your extensions will be added to the schema on startup of the IdentityHub server.

Adding custom fields

A common use case could be a custom attribute your company is using to store some information on a User in LDAP. The example below shows how you can add this attribute to the User type and therefore make it accessible in your queries. To show how to add a schema extension directly in the config instead of a separate file the corresponding syntax is used here.

toml
[graphql]
  schemaExtension='''
    extend type User {
      companyAttribute: String @ldapField(attribute: "companyAttribute")
    }
  '''

For multiline strings in the config file use the triple '''.

After restarting the IdentityHub you are now able to request the field companyAttribute on every User type and the Viewer as well.

In the schema extension above we defined this field to return a single value string, using the @ldapField directive which takes care of extracting the value from the LDAP attribute companyAttribute and casting it to the desired type.

For multi-value fields adapt your schema extension to return an array of strings. Since the passed parameter attribute is the same as the alias at the start of the line, you don't need to pass the attribute parameter.

graphql
extend type User {
  companyAttribute: [String] @ldapField
}

Filter by custom attributes

In the same way as seen above, you can extend input types. For example you may want to be able to filter your users by your newly added field. The extension below shows how to extend the UserBy input type and how to use it in a query.

graphql
extend input UserBy {
  companyAttribute: [String!]
}

We defined the companyAttribute input field as an array of strings because this automatically provides you with the possibility to create OR connected filters as seen below.

query SearchUsers {
  users(by: { companyAttribute: ["value1", "value2"] }) {
    nodes {
      id
      companyAttribute
    }
  }
}

This query will create the LDAP filter

(&(objectClass=Person)(|(companyAttribute=value1)(companyAttribute=value2)))
TIP

To stay compliant with the already existing API you may want to additionally extend the UserWhere and add the companyAttribute to the qAttributes in the config section ldap.users.qAttributes.

graphql
extend input UserWhere {
  companyAttribute: [StringFilterInput!]
}

extend input UserBy {
  companyAttributes: [String]
}

Those extensions will give you the ability to perform the following queries

graphql
query SearchUsersWithWhere {
  users(where: { companyAttribute: { startsWith: "a" } }) {
    nodes {
      id
      companyAttribute
    }
  }
}
graphql
query SearchUsersWithBy {
  usersBy(companyAttributes: ["value1", "value2"]) {
    nodes {
      id
      companyAttribute
    }
  }
}
TIP

We highly recommend to define your FilterInputs as arrays (i.e. givenName: [StringMatchFilterInput!] instead of givenName: StringMatchFilterInput), to take advantage of implicit combination of filters from arrays with or.

You can even use aliases for the added attributes by using the ldap.attributeMapping config option. See the example below on how to add a filter surname that effectively filters by ldaps sn attribute.

toml
[ldap]
  [ldap.attributeMapping]
    surname="sn"
[graphql]
  schemaExtension='''
    extend inputUserWhere {
      surname: [StringMatchFilterInput!]
    }
  '''

Now you can write a query like:

graphql
query SearchUsersWithWhere {
  users(where: { surname: { eq: "mustermann" } }) {
    nodes {
      id
      surname
    }
  }
}
WARNING

Keep in mind, that you only can extend a type but not override fields of that type. If a type you want to extend already has a givenName property, you cannot re-define that property and you have to use another name for that field.

Create new types and queries

Another common use case may be, that you want to define a whole new LDAP type and with that queries to find those types in your directory. Let's say you store objects called Applications in your directory, that have a subset of custom properties as well as relations to Groups. First, we define the new type Application.

graphql
type Application {
  id: ID! @ldapField(attribute: "guid") @id
  name: String @ldapField(attribute: "appName")
  title: String @ldapField(attribute: "appTitle")
  availableFor: GroupConnection @ldapField(attribute: "groups") @resolveDNConnection(resolvedType: "Group")
}

We created a new type Application, that uses the guid attribute as its ID, has a name and title extracted from the given LDAP attributes and has a pageable Connection of groups connected to it, which are stored in a multi-value attribute groups, which we first resolve via the @ldapField directive and the transform to a Connection with the @resolveDNConnection directive.

To properly work with our new type we may need a query to be able to find them via the IdentityHub. We will add a simple q filter and basic ldap paging possibilities. To ensure a proper Connection we need to define the corresponding ApplicationConnection and ApplicationEdge types. The type Query is just another GraphQL type that we can extend.

graphql
type ApplicationEdge {
  cursor: ConnectionCursor!
  node: Application
}

type ApplicationConnection {
  estimatedSize: Int
  edges: [ApplicationEdge!]!
  nodes: [Application!]!
  pageInfo: PageInfo!
}

extend type Query {
  applications(q: String, paging: LDAPPaging) ApplicationConnection @ldapSearch(filter: "(objectClass=application)", scope: sub, base: "ou=applications,o=data", qAttributes: ["appName", "appTitle"])
}

The @ldapSearch directive creates a search function for you and let's you define base and scope, as well as a custom filter that narrows down the results. by defining a q parameter and providing the qAttributes we can now find our applications by their name of title.

graphql
query SearchApplications {
  applications(q: "test") {
    nodes {
      id
      name
      title
    }
  }
}

This will perform the LDAP query

(&(objectClass=application)(|(appName=test)(appTitle=test)))

under the base ou=applications,o=data with the search scope sub.

Conclusion

These examples should give you a short glimpse of the powerful, yet easy ways to use schema extensions.

For further insight, please use the schema explorer of the graphql playground (if configured the IdentityHub to expose it, i.e. http://localhost:4000/-/playground) to explore the schema after you extended it via config.

For a detailed description of all directives or guidance along your way, please don't hesitate to contact us.