Transposing the Object Model in an Identity Cloud Migration

Starting a brand new project in ForgeRock Identity Cloud is possibly the easiest path - one that allows to stick to best practices from the beginning, with minimum customisations by using the out of the box features. Migrating a self-managed ForgeRock deployment to ForgeRock Identity Cloud in the other hand brings new challenges as now the organisation has to find the proper compromise between minimum applications impact while staying within the ForgeRock Identity Cloud best practices.

This article focusses in particular to the Identity Management object model. In a self managed deployment, there is no constraints in the object model design, except performance considerations. While in Identity Cloud, the object model design must stay within constraints, not all features of a self managed platform are available in ForgeRock Identity Cloud.

UNDERSTANDING THE FORGEROCK IDENTITY CLOUD OBJECT MODEL

The constraints

In a self managed environment, one can:

  1. Create custom object types in identity management
  2. Create new relationships between object types.
  3. Extend the Identity Store schema

While in ForgeRock Identity Cloud, only the first item is allowed.

Furthermore, since there is no access to the file system in Identity Cloud, features that require referencing a script on file system are not available. This means that all scripts are to be inserted inline in the configuration. In particular, it is not possible to configure custom validation policies.

The ForgeRock Identity Cloud schema

The default Schema in ForgeRock Identity Cloud is similar in structure to a a self managed 7.3 schema, with the following characteristics:

  • There is one default schema per realm (Alpha and Bravo), where the default object types are prefixed with the realm name; e.g User becomes Alpha_user or Bravo_user, Role becomes Alpha_role or Bravo_role, and so on.
  • Schema extensions should be made within the rules described at IDM managed object schema.

You can find a summary of the Identity Schema here: Identity schema summary

In this paper, we will look at the artefacts available to transpose optimally the object model. Those are:

  • The Generic Extension attributes
  • Custom Attributes
  • Custom Object Types.

Object Modelling should take into account the latest enhancements to the Identity Platform as an opportunity to modernise the solution when migrating to ForgeRock Identity cloud, for example, leveraging from Organisations and Groups.

Impact to client applications and configurations

You need to consider the following points contemplating a migration to ForgeRock Identity Cloud:

  • Transposing the model to Identity Cloud has an impact to the applications interfacing with the Identity Management API - due to the renaming of the resource names and their properties. This can be alleviated to some extent by implementing custom endpoints to adapt to the new object model.

  • Identity Management customisations (such as onUpdate, onCreate scripts and so on) are likely impacted.

  • Access Management configurations that references attributes of a custom Identity Store schema need to be re-designed.

Model complexity/Customisation Efforts

Building a model bound to the current self-management design may lead to substantial customisation efforts, and complexity. It might be therefore worth to pause and step back, and rethink through the business requirements and design in a way that leads to a streamlined and frictionless move to Identity Cloud - in other word, like a fresh start.

So there is a trade off between a “like to like” approach with minimal impact to the integrations but increased maintenance cost in the long term due to complexity, versus fresh start with application migration effort in the short term but less maintenance costs in the future.

TRANSLATING TO THE FORGEROCK IDENTITY CLOUD OBJECT MODEL

So let’s enumerate the factors to consider that will contribute to a successful transition.

Revisit the business requirements

Migrating to ForgeRock Identity Cloud is the opportunity to revisit the business requirements, and how they have been translated to the design. To ensure success, it is advisable to thoroughly analyse the current configurations, and answer the following questions at a high level:

  • In which way the current design respond to the business requirements? Is it still valid?
  • Could it be done differently? This particularly applies to features that are not translatable to ForgeRock Identity Cloud.
  • Could business requirements be revisited in order to simplify the design?

These questions does not need to be answered straight at the start of the project, but rather left open while devising the new object model, and as such allows for an iterative process.

Identities Partitioning

Having different Identity populations will translate to several realms in the solution, meaning, several ForgeRock Identity Cloud tenants each hosting 2 realms. In this case, it might be worth contemplating wether user identities could be consolidated in order to reduce hosting costs. On the other hand consolidating user identities may come with higher development complexity (if users still need to be differentiated) and operational costs in actually re-structuring the user identities. So there is a trade off to consider.

Characterise the identity data

In order to translate the object model, we need to characterise the user identity data according to these categories:

  1. Essential to the security solution
  2. Used to identify uniquely users
  3. Referenced from Access Management configurations

Essential Data

The essential data is the information that is needed to authenticate/identify users and to protect application access, nothing less, nothing more. In particular, it could be tempting to add information in the user profiles, that is not necessarily involved in the security flows - but rather are used by applications to deliver the business value. With a self-managed deployment, this would not be an issue - there are no limitations, but in ForgeRock Identity Cloud the number of generic extension attributes are limited. These are listed at General purpose extension attributes. This means, as much as possible, do not store non essential data in a generic extension attribute, rather use a custom attribute, if it is not possible to store the data outside of ForgeRock Identity Cloud.

How Identity Data is searched

To authenticate a user, the journey implementation uses the data provided by the user - often it is the email address. Finding a user requires searching in the Identity Store using a searchable attribute - that is one that is “indexed”; if not, this can render the environment unresponsive, and more so with a large user population. The out of the box userName, or email address attributes are indexed and a natural choice. There might be other user experiences involving other means of identification - for example a mobile application might use a device id. Since there are 5 Indexed string attributes, if the solution requires more than 5, then it would be worth reviewing the requirements / design, as well as the security posture.

Nonetheless, if it is still really necessary to have so many means of identification, and there are not enough indexed attributes to hold them, then the fallback is to use Custom Types.

Access Management configuration

Here are some consideration in designing the object model keeping in mind the migration of the current configurations in the Access Management solution.

From within a journey, users must be identified, and the authentication process might need additional information to validate the user. This is also the case with OAuth2 authorisation flows - accessing user data information for scope validation, and access token content, for example. The same applies to a SAML2 attribute mapper.

All the user information can be obtained directly from the Identity Management API, regardless of how it is stored - whether it is in a custom attribute, generic extension attribute or custom object type.

When it comes to Access Management, where the information is stored matters, so let’s examine each case.

Generic Extension Attribute

Using a generic extension attribute is the ideal solution as it is directly accessible from the user profile (through the idRepository object), as well as can be referenced from out of the box tree nodes such as the Identity Management nodes. They are also available to a SAML2 attribute mapper.

There are two main categories: Indexed attributes, and Unindexed attributes. If the attribute is used to search for an identity, then an indexed attribute must be used, otherwise use an unindexed attribute.

The list of attributes available in the user profile, and how they are named respectively in Identity Management and Access Management, is provided at : General purpose extension attributes

Custom Attributes

Custom Attributes are not directly accessible from the user profile without some conversion. Custom Attributes are all stored in a single attribute in JSON format - accessing individual attributes require parsing the JSON string, accessing the attribute value with its key (the attribute name), and updating requires to update the JSON object and to serialise it before storing. This method is not documented, and as such, has some risks since that scheme could be changed in the future.

The second method to access Custom Attributes from Access Management M is to invoke the Identity Management API. This requires obtaining an access token and issue a call to managed service from an Access Management customisation script.

A custom attribute can not be referenced from an out of the box authentication node - only from a script, and can’t be accessed ether from a SAML2 attribute mapper.

And if an attribute is used to search for an identity, then it can’t be stored in a custom attribute.

Custom Object Type

Data stored in a custom object type can only be accessed via a call to the Identity Management API - so only from a customisation script.

Use one for many

And finally, when all extension attributes are leveraged, an option is to convert one of the string extension attribute to hold a JSON object that gathers all the custom attributes. From Access Management perspectives, the involved customisations are the same as accessing custom attributes from the user profile, however, it is safer as this will never be subject to change.

An attribute stored with this method can’t be searched though, as the matching rule would apply to the entire JSON string.

Relationships

One-to-Many versus Many-to-Many

Relationships is a challenging subject, as creating new relationships in ForgeRock Identity Cloud is not permitted. But before dwelling in a translation method, a good question needs to be asked: does Identity Management really needs to store related objects? There are basically two types of relationships a user can be involved to: one to many, and many to many. There are very few use cases in an identity management solution that would require a many to many relationship other than Roles, Groups and Organisations. And in fact, are these really relevant to the identity space?

One obvious dependency relevant to the Identity domain are user owned devices. Data such as associated primary keys or other confidential data related to the user is another possible dependency. In all cases, this data is never shared between users, and as such, the model is a One to Many relationship in this case.

How does it matter to the translation? In the relational database world, a many to many relationship is represented by an intermediary table whose rows hold primary keys to the related entities. This is actually what does the Relationship objects in the Identity Management internal schema. Now, if one needs to design a many to many relationship, then 2 additional custom objects are needed to actualise the relation, which incur an additional roundtrip in accessing user information - and therefore, impacts performances.

When the relation is One to Many, no need for an intermediate object, just store the user Id in the related object.

Searchable versus Non searchable

If the dependant objects do not need to hold searchable attributes (for example, there is no use case for searching a user from its device) then no need necessarily to create a new object type. The information could be stored as an array of objects in a custom attribute, or in a multi-valued extension attribute as an array of JSON strings, or in a generic string extension attribute (as a JSON string). Which method to use depends entirely in whether, and how, the information is obtained from AM.

In the other hand, if dependant objects need to be searched, then create a custom object, and link it to the user with the user id. All attributes in a custom type are searchable. The drawback here is that in order to access this information from AM, a request must be made to the IDM managed endpoint.

Application Integrations

So there are two things that impact the application integration:

  • Change in object and attribute names
  • Relationship traversal

Identity Management API versus AM Journey

The largest impact is to client applications interfacing directly with the Identity Management API. With ForgeRock Identity Cloud, identity management can be orchestrated from an AM journey. This is the opportunity to weight the costs between:

  • Migrating client applications to leverage the new object model, or
  • Migrating the identity orchestration to AM journeys.

The former approach is consistent with a “like to like” approach, where the latter is the result of reviewing entirely the design (and possibly business requirements).

Limiting the impact to client applications

If you choose the path of migrating client applications to leverage the ForgeRock Identity Cloud model, then it might be possible to minimise the development effort by implementing a custom endpoint that translates the legacy request into IDM internal calls, and to translate the response back to the format expected by the client application.

A practical example

So here is an example. Let’s assume the legacy model includes a devices relationship to a custom object type Device. The client application uses this REST call : GET managed/user/<ID>?_fields=*, devices/* to retrieve user information. As long as the client application does not rely on the relation properties, the custom endpoint can forge the response expected by the client application. So originally, a response would have typically been something like:

{
    "_id": "a5323ca4-afb3-4bb9-91d5-dc65dd104d72",
    "_rev": "c2320801-0ca6-467b-a79e-31aa8c94cdc1-2317",
    "mail": "john@example.com",
    "givenName": "john",
    "sn": "smith",
    "userName": "john",
    "position" : "Direct Creative Developer",
    ...,
    "devices": [
        {
            "_rev": "c2320801-0ca6-467b-a79e-31aa8c94cdc1-2308",
            "_id": "8678a916-0cdb-493f-832e-8465eeb4cf95",
            "_ref": "managed/device/8678a916-0cdb-493f-832e-8465eeb4cf95",
            "_refResourceCollection": "managed/device",
            "_refResourceId": "8678a916-0cdb-493f-832e-8465eeb4cf95",
            "_refProperties": {
                "_id": "c1e719e4-bddb-42f7-96f0-825ef79d9231",
                "_rev": "c2320801-0ca6-467b-a79e-31aa8c94cdc1-2315"
            },
            "type" : "Android",
            "pin" : "38DCC6F5-CAD9-B20C-BA76-C6E22555FDAA"
        },
        {
            "_rev": "c2320801-0ca6-467b-a79e-31aa8c94cdc1-2291",
            "_id": "35da9366-13b7-418a-88f5-713014e9d8c9",
            "_ref": "managed/device/35da9366-13b7-418a-88f5-713014e9d8c9",
            "_refResourceCollection": "managed/device",
            "_refResourceId": "35da9366-13b7-418a-88f5-713014e9d8c9",
            "_refProperties": {
                "_id": "d41f7b12-4867-4420-8298-f2de63e981e4",
                "_rev": "c2320801-0ca6-467b-a79e-31aa8c94cdc1-2298"
            },
            "type" : "iOS",
            "pin" : "4786A3DA-EA03-96F6-4100-D860BA09023D"
        }
    ]
}
The mapping

A natural translation is to translate User to Alpha_user and create a Device custom Object including a userId attribute set to the id of owner.
Then the position attribute is not an out of the box attribute in the default schema; since it is not used for identification, let’s translate it to frUnindexedString1.

The custom endpoint

Some detailed information is available in ForgeRock documentation: Create custom endpoints to launch scripts.

As long as the client application does not rely on the relation properties (e.g _refProperties), then a custom endpoint could respond to the client application request as is (provided a reverse proxy can dispatch to the new endpoint) and forge the response expected by the client application. Even properties like _ref could be generated in the response. So here is a sample that covers the read and the query operations:

(function () {
  function get_devices(id) {
    let obj = openidm.query("managed/device", {
      _queryFilter: 'userId eq "' + id + '"',
    });
    for (let i in obj.result) {
      delete obj.result[i].userId;
    }
    return obj.result;
  }

  function translate(obj) {
    return {
      _id: obj._id,
      _rev: obj._id,
      mail: obj.mail,
      devices: obj.devices,
      position: obj.frUnindexedString1,
      sn: obj.sn,
      userName: obj.userName,
      givenName: obj.givenName,
    };
  }

  if (request.method === "query") {
    if (
      typeof request.queryFilter !== "undefined" &&
      request.queryFilter !== null &&
      typeof request.pageSize !== "undefined" &&
      request.pageSize !== 0
    ) {
      params = { _queryFilter: request.queryFilter.toString() };
      params._pageSize = request.pageSize;
      if (
        typeof request.pagedResultsCookie !== "undefined" &&
        request.pagedResultsCookie !== null
      ) {
        params._pagedResultsCookie = request.pagedResultsCookie;
      }
      if (
        typeof request.pagedResultsOffset !== "undefined" &&
        request.pagedResultsOffset !== null
      ) {
        params._pagedResultsOffset = request.pagedResultsOffset;
      }
      let result = openidm.query("managed/alpha_user", params);
      for (let i in result.result) {
        let id = result.result[i]._id;
        result.result[i].devices = get_devices(id);
        result.result[i] = translate(result.result[i]);
      }
      return result;
    }
  } else if (request.method === "read") {
    let obj = openidm.read("managed/alpha_user/" + request.resourcePath);
    if (obj === null) {
      throw {
        code: 404, // any valid HTTP error code
        reason: "Not Found",
        message: "Resource with ID: " + request.resourcePath + " not found.",
      };
    }
    obj.devices = get_devices(request.resourcePath);
    returnObj = translate(obj);
    return returnObj;
  }
  throw { code: 500, message: "Unsupported request" };
})();

The corresponding configuration is (with the script in source):

{
  "_id": "endpoint/alpha_user",
  "type": "text/javascript",
  "source": "(function () {\n  ......",
  "description": "",
  "context": "endpoint/managed/user/*"
}

Note the context: it is not possible to currently set it with the admin console, so you’ll have to set it using a REST call to the config endpoint (config/endpoint/alpha_user) or use frodo. First configure an endpoint with the name alpha_user, paste in the script content, save. Then retrieve the configuration via REST, and change the context to the proper value, use then a REST call to push back the modified object. Now the endpoint will respond GET enpoint/managed/user as well as GET enpoint/managed/user/<ID>

The result for a user then looks like this:

{
    "_id": "b310502d-5ab3-4adc-aca6-f1ec2916caca",
    "_rev": "b310502d-5ab3-4adc-aca6-f1ec2916caca",
    "mail": "Magaly.Grimes@example.com",
    "devices": [
        {
            "type": "Android",
            "pin": "38DCC6F5-CAD9-B20C-BA76-C6E22555FDAA",
            "_rev": "c2320801-0ca6-467b-a79e-31aa8c94cdc1-5743",
            "_id": "f93683ba-33a8-4442-afd9-288427bee81c"
        },
        {
            "type": "iOS",
            "pin": "4786A3DA-EA03-96F6-4100-D860BA09023D",
            "_rev": "c2320801-0ca6-467b-a79e-31aa8c94cdc1-5795",
            "_id": "89226a1a-cc39-4415-bd5e-f8db5709151b"
        }
    ],
    "givenName": "Magaly",
    "position": "Direct Creative Developer",
    "sn": "Grimes",
    "userName": "Magaly.Grimes"
}

The principles remain the same for implementing updates, split the original request into internal requests based on the updated attributes - then aggregate back the response, transforming attribute names if necessary, from the request to internal, and from the internal object to the final response.

CONCLUSION

The most crucial success element in migrating to Forgerock Identity Cloud is to thoroughly review the organisation requirements and the corresponding solution before embarking into this adventure. If this is done properly, then the transition should be greatly facilitated, with an optimum result, and this article vowed to give you the elements to facilitate this process.

6 Likes