Uploaded image for project: 'OpenDJ'
  1. OpenDJ
  2. OPENDJ-7789

Rest2ldap needs a way to access an object with high cardinality references

    XMLWordPrintable

    Details

    • Epic
    • Status: In Progress
    • Blocker
    • Resolution: Duplicate
    • 2021.Summer
    • 2021.Summer
    • common-repo, performance, rest
    • None
    • Rest2Ldap high-cardinality refs
    • Large
    • In Progress
    • 70
    • N/A

      Description

      How the edge endpoints (managed/user/bob/roles) work in IDM

      Currently, when accessing an object via rest2ldap that has high cardinality on its references you end up with an entry that can have an array field with hundreds or many thousands of entries. For example, this is the response for querying a user with 50 roles.

      curl --location --request GET 'http://localhost:8080/openidm/managed/user/bob?_fields=roles' \
      --header 'X-OpenIDM-Username: openidm-admin' \
      --header 'X-OpenIDM-Password: openidm-admin' \
      --header 'Content-Type: application/json'
      
      {
          "_id": "bob",
          "_rev": "00000000596569be",
          "roles": [
              {
                  "_ref": "managed/role/role-1",
                  "_refResourceCollection": "managed/role",
                  "_refResourceId": "role-1",
                  "_refProperties": {
                      "_id": "4e5a85d5-77a7-4d4c-a321-5fb2c4dfdbd3",
                      "_rev": "cdb127f507842a7b78d583996eb83ffea09078bb69e8252729e72d20ddd033e7"
                  }
              },
      ...
              {
                  "_ref": "managed/role/role-50",
                  "_refResourceCollection": "managed/role",
                  "_refResourceId": "role-50",
                  "_refProperties": {
                      "_id": "0aeb66c0-edfb-4b69-b6ec-95d556c5942a",
                      "_rev": "0c2f3f1232b2bc64b4af014e5420df5eac121e0cffdeeab453da498e649eb85a"
                  }
              }
          ]
      }
      

      Part of the roles array is cut out for brevity, but there would be 50 entries in the roles array. This number could potentially be much higher in a real environment.

      It is currently not feasible to get back an object that has high cardinality references. IDM identified this problem in its product, and as a solution invented an endpoint that allows the management of relationship fields on a distinct object directly.

      This endpoint has the resource path 'managed/user/id/field'. For example, given a resource path like so:

      managed/user/bob/roles

      A client is able to manage the roles relationship entries for the user bob. This endpoint allows the ability to create, update, delete, read, and query the roles field for managed user bob. Instead of getting back the bob object with a roles field containing 3000 entries, the client instead gets back each of the role relationships in individual entries allowing for the paging of the entries in the roles field. These are examples of the relationship endpoint using the user bob and roles example.

      IDM Relationship Endpoints

      IDM has invented some reference/relationship endpoint that allows us to interact with relationships, which allows us to page through high cardinality relationships.

      Create

      Create on the relationship endpoint allows for the creation of a relationship on user bob for the roles field. For example, doing this operation:

      curl --location --request POST 'http://localhost:8080/openidm/managed/user/bob/roles?_action=create' \
      --header 'X-OpenIDM-Username: openidm-admin' \
      --header 'X-OpenIDM-Password: openidm-admin' \
      --header 'Content-Type: application/json' \
      --data-raw '{
          "_ref" : "managed/role/role-30",
          "_refProperties" : {
              "key" : "value"
          }
      }'
      
      {
          "_id": "61c4590f-2da6-4bcb-9fd9-7378956ca12e",
          "_rev": "cf26bcaa3e6d9466c8a80e973801790141878ec1fb8f40bd847f601765badd65",
          "_ref": "managed/role/role-30",
          "_refResourceCollection": "managed/role",
          "_refResourceId": "role-30",
          "_refProperties": {
              "key": "value",
              "_id": "61c4590f-2da6-4bcb-9fd9-7378956ca12e",
              "_rev": "cf26bcaa3e6d9466c8a80e973801790141878ec1fb8f40bd847f601765badd65"
          }
      }
      

      creates a roles relationship on user bob as seen in this query

      curl --location --request GET 'http://localhost:8080/openidm/managed/user/bob/roles/4983de87-5428-4f18-ab2f-1896b76c5575' \
      --header 'X-OpenIDM-Username: openidm-admin' \
      --header 'X-OpenIDM-Password: openidm-admin' \
      --header 'Content-Type: application/json' 
      
      {
          "_id": "4983de87-5428-4f18-ab2f-1896b76c5575",
          "_rev": "cf26bcaa3e6d9466c8a80e973801790141878ec1fb8f40bd847f601765badd65",
          "_ref": "managed/role/role-30",
          "_refResourceCollection": "managed/role",
          "_refResourceId": "role-30",
          "_refProperties": {
              "_id": "4983de87-5428-4f18-ab2f-1896b76c5575",
              "_rev": "cf26bcaa3e6d9466c8a80e973801790141878ec1fb8f40bd847f601765badd65",
              "key": "value"
          }
      }
      

      Update

      Update on the relationship endpoint allows for the update of a relationship's properties on a specified relationship. For example, doing this operation

      curl --location --request PUT 'http://localhost:8080/openidm/managed/user/bob/roles/4983de87-5428-4f18-ab2f-1896b76c5575' \
      --header 'X-OpenIDM-Username: openidm-admin' \
      --header 'X-OpenIDM-Password: openidm-admin' \
      --header 'Content-Type: application/json' 
      --data-raw '{
          "_ref" : "managed/role/role-30",
          "_refProperties" : {
              "key" : "newValue"
          }
      }'
      

      updates the properties on the specified relationship as seen in this query

      curl --location --request GET 'http://localhost:8080/openidm/managed/user/bob/roles/4983de87-5428-4f18-ab2f-1896b76c5575' \
      --header 'X-OpenIDM-Username: openidm-admin' \
      --header 'X-OpenIDM-Password: openidm-admin' \
      --header 'Content-Type: application/json'
      
      {
          "_id": "4983de87-5428-4f18-ab2f-1896b76c5575",
          "_rev": "2fd41b25b704cb2a4c0686de6b0f4be669bc18ff2afef2978e115cf167eaf402",
          "_ref": "managed/role/role-30",
          "_refResourceCollection": "managed/role",
          "_refResourceId": "role-30",
          "_refProperties": {
              "_id": "4983de87-5428-4f18-ab2f-1896b76c5575",
              "_rev": "2fd41b25b704cb2a4c0686de6b0f4be669bc18ff2afef2978e115cf167eaf402",
              "key": "newValue"
          }
      }
      

      Delete

      Delete on the relationship endpoint deletes the given relationship from user bob. For example, doing this operation

      curl --location --request DELETE 'http://localhost:8080/openidm/managed/user/bob/roles/4983de87-5428-4f18-ab2f-1896b76c5575' \
      --header 'X-OpenIDM-Username: openidm-admin' \
      --header 'X-OpenIDM-Password: openidm-admin' \
      --header 'Content-Type: application/json'
      
      {
          "_id": "4983de87-5428-4f18-ab2f-1896b76c5575",
          "_rev": "2fd41b25b704cb2a4c0686de6b0f4be669bc18ff2afef2978e115cf167eaf402",
          "_ref": "managed/role/role-30",
          "_refResourceCollection": "managed/role",
          "_refResourceId": "role-30",
          "_refProperties": {
              "_id": "4983de87-5428-4f18-ab2f-1896b76c5575",
              "_rev": "2fd41b25b704cb2a4c0686de6b0f4be669bc18ff2afef2978e115cf167eaf402",
              "key": "newValue"
          }
      }
      

      deletes the given relationship from user bob as seen in this query

      curl --location --request GET 'http://localhost:8080/openidm/managed/user/bob/roles/4983de87-5428-4f18-ab2f-1896b76c5575' \
      --header 'X-OpenIDM-Username: openidm-admin' \
      --header 'X-OpenIDM-Password: openidm-admin' \
      --header 'Content-Type: application/json'
      
      {
          "code": 404,
          "reason": "Not Found",
          "message": "Object relationships/4983de87-5428-4f18-ab2f-1896b76c5575 not found in relationships"
      }
      

      Read

      Read on the relationship endpoint reads the given relationship from user bob. For example, doing this operation

      curl --location --request GET 'http://localhost:8080/openidm/managed/user/bob/roles/4e5a85d5-77a7-4d4c-a321-5fb2c4dfdbd3?_fields=/,/members/*' \
      --header 'X-OpenIDM-Username: openidm-admin' \
      --header 'X-OpenIDM-Password: openidm-admin' \
      --header 'Content-Type: application/json'
      
      {
          "_id": "4e5a85d5-77a7-4d4c-a321-5fb2c4dfdbd3",
          "_rev": "cdb127f507842a7b78d583996eb83ffea09078bb69e8252729e72d20ddd033e7",
          "name": "role-1",
          "description": "Role 1",
          "members": [
              {
                  "_rev": "00000000b4adf06d",
                  "_id": "bob",
                  "userName": "user-50",
                  "givenName": "givenName-50",
                  "sn": "sn-50",
                  "mail": "my.email.50@example.com",
                  "accountStatus": "active",
                  "effectiveRoles": [
                      {
                          "_refResourceCollection": "managed/role",
                          "_refResourceId": "role-1",
                          "_ref": "managed/role/role-1"
                      },
                      ...
                      {
                          "_refResourceCollection": "managed/role",
                          "_refResourceId": "role-50",
                          "_ref": "managed/role/role-50"
                      }
                  ],
                  "effectiveAssignments": [],
                  "_ref": "managed/user/bob",
                  "_refResourceCollection": "managed/user",
                  "_refResourceId": "bob",
                  "_refProperties": {
                      "_id": "4e5a85d5-77a7-4d4c-a321-5fb2c4dfdbd3",
                      "_rev": "cdb127f507842a7b78d583996eb83ffea09078bb69e8252729e72d20ddd033e7"
                  }
              }
          ],
          "_refResourceCollection": "managed/role",
          "_refResourceId": "role-1",
          "_refResourceRev": "00000000823f47f9",
          "_ref": "managed/role/role-1",
          "_refProperties": {
              "_id": "4e5a85d5-77a7-4d4c-a321-5fb2c4dfdbd3",
              "_rev": "cdb127f507842a7b78d583996eb83ffea09078bb69e8252729e72d20ddd033e7"
          }
      }
      

      reads the given relationship from user bob. Read also allows the relationship expansion of the object the relationship points to. This expansion can be done by specifying the fields on the referred-to object the client wants to be expanded.

      Query

      Query allows the query of all the relationships for a given field on user bob. For example, doing this operation

      curl --location --request GET 'http://localhost:8080/openidm/managed/user/bob/roles?_queryFilter=true&_fields=/,/members' \
      --header 'X-OpenIDM-Username: openidm-admin' \
      --header 'X-OpenIDM-Password: openidm-admin' \
      --header 'Content-Type: application/json' 
      
      {
          "result": [
              {
                  "_id": "4e5a85d5-77a7-4d4c-a321-5fb2c4dfdbd3",
                  "_rev": "cdb127f507842a7b78d583996eb83ffea09078bb69e8252729e72d20ddd033e7",
                  "name": "role-1",
                  "description": "Role 1",
                  "members": [
                      {
                          "_ref": "managed/user/bob",
                          "_refResourceCollection": "managed/user",
                          "_refResourceId": "bob",
                          "_refProperties": {
                              "_id": "4e5a85d5-77a7-4d4c-a321-5fb2c4dfdbd3",
                              "_rev": "cdb127f507842a7b78d583996eb83ffea09078bb69e8252729e72d20ddd033e7"
                          }
                      }
                  ],
                  "_refResourceCollection": "managed/role",
                  "_refResourceId": "role-1",
                  "_refResourceRev": "00000000823f47f9",
                  "_ref": "managed/role/role-1",
                  "_refProperties": {
                      "_id": "4e5a85d5-77a7-4d4c-a321-5fb2c4dfdbd3",
                      "_rev": "cdb127f507842a7b78d583996eb83ffea09078bb69e8252729e72d20ddd033e7"
                  }
              },
      ...
              {
                  "_id": "0aeb66c0-edfb-4b69-b6ec-95d556c5942a",
                  "_rev": "0c2f3f1232b2bc64b4af014e5420df5eac121e0cffdeeab453da498e649eb85a",
                  "name": "role-50",
                  "description": "Role 50",
                  "members": [
                      {
                          "_ref": "managed/user/bob",
                          "_refResourceCollection": "managed/user",
                          "_refResourceId": "bob",
                          "_refProperties": {
                              "_id": "0aeb66c0-edfb-4b69-b6ec-95d556c5942a",
                              "_rev": "0c2f3f1232b2bc64b4af014e5420df5eac121e0cffdeeab453da498e649eb85a"
                          }
                      }
                  ],
                  "_refResourceCollection": "managed/role",
                  "_refResourceId": "role-50",
                  "_refResourceRev": "00000000406d495f",
                  "_ref": "managed/role/role-50",
                  "_refProperties": {
                      "_id": "0aeb66c0-edfb-4b69-b6ec-95d556c5942a",
                      "_rev": "0c2f3f1232b2bc64b4af014e5420df5eac121e0cffdeeab453da498e649eb85a"
                  }
              }
          ],
          "resultCount": 50,
          "pagedResultsCookie": null,
          "totalPagedResultsPolicy": "NONE",
          "totalPagedResults": -1,
          "remainingPagedResults": -1
      }
      

      returns all the roles for user bob. Query allows you to do 3 things that are very important
      1. The client can query filter on the relationship properties and the referred-to objects fields. For example, this query allows us to match specific role relationships on user bob.

      curl --location --request GET 'http://localhost:8080/openidm/managed/user/bob/roles?_queryFilter=_ref%20eq%20%22managed/role/role-2%22&_fields=/,/members' \
      --header 'X-OpenIDM-Username: openidm-admin' \
      --header 'X-OpenIDM-Password: openidm-admin' \
      --header 'Content-Type: application/json' 
      
      {
          "result": [
              {
                  "_id": "fda3bada-82b7-439f-8839-afc231fdd672",
                  "_rev": "10aaf9922670e7b31719067e32592ece2632beaf97585dc73660450c42a7dc89",
                  "name": "role-2",
                  "description": "Role 2",
                  "members": [
                      {
                          "_ref": "managed/user/bob",
                          "_refResourceCollection": "managed/user",
                          "_refResourceId": "bob",
                          "_refProperties": {
                              "_id": "fda3bada-82b7-439f-8839-afc231fdd672",
                              "_rev": "10aaf9922670e7b31719067e32592ece2632beaf97585dc73660450c42a7dc89"
                          }
                      }
                  ],
                  "_refResourceCollection": "managed/role",
                  "_refResourceId": "role-2",
                  "_refResourceRev": "00000000869a4909",
                  "_ref": "managed/role/role-2",
                  "_refProperties": {
                      "_id": "fda3bada-82b7-439f-8839-afc231fdd672",
                      "_rev": "10aaf9922670e7b31719067e32592ece2632beaf97585dc73660450c42a7dc89"
                  }
              }
          ],
          "resultCount": 1,
          "pagedResultsCookie": null,
          "totalPagedResultsPolicy": "NONE",
          "totalPagedResults": -1,
          "remainingPagedResults": -1
      }
      

      2. The client can page over the relationships. For an object with a high cardinality of relationships being able to page over those relationships instead of just returning them all in one large field is critical. For example, with this query, we can get a few roles at a time from user bob.

      curl --location --request GET 'http://localhost:8080/openidm/managed/user/bob/roles?_queryFilter=true&_pageSize=3&_fields=/,/members' \
      --header 'X-OpenIDM-Username: openidm-admin' \
      --header 'X-OpenIDM-Password: openidm-admin' \
      --header 'Content-Type: application/json'
      
      {
          "result": [
              {
                  "_id": "4e5a85d5-77a7-4d4c-a321-5fb2c4dfdbd3",
                  "_rev": "cdb127f507842a7b78d583996eb83ffea09078bb69e8252729e72d20ddd033e7",
                  "name": "role-1",
                  "description": "Role 1",
                  "members": [
                      {
                          "_ref": "managed/user/bob",
                          "_refResourceCollection": "managed/user",
                          "_refResourceId": "bob",
                          "_refProperties": {
                              "_id": "4e5a85d5-77a7-4d4c-a321-5fb2c4dfdbd3",
                              "_rev": "cdb127f507842a7b78d583996eb83ffea09078bb69e8252729e72d20ddd033e7"
                          }
                      }
                  ],
                  "_refResourceCollection": "managed/role",
                  "_refResourceId": "role-1",
                  "_refResourceRev": "00000000823f47f9",
                  "_ref": "managed/role/role-1",
                  "_refProperties": {
                      "_id": "4e5a85d5-77a7-4d4c-a321-5fb2c4dfdbd3",
                      "_rev": "cdb127f507842a7b78d583996eb83ffea09078bb69e8252729e72d20ddd033e7"
                  }
              },
              {
                  "_id": "fda3bada-82b7-439f-8839-afc231fdd672",
                  "_rev": "10aaf9922670e7b31719067e32592ece2632beaf97585dc73660450c42a7dc89",
                  "name": "role-2",
                  "description": "Role 2",
                  "members": [
                      {
                          "_ref": "managed/user/bob",
                          "_refResourceCollection": "managed/user",
                          "_refResourceId": "bob",
                          "_refProperties": {
                              "_id": "fda3bada-82b7-439f-8839-afc231fdd672",
                              "_rev": "10aaf9922670e7b31719067e32592ece2632beaf97585dc73660450c42a7dc89"
                          }
                      }
                  ],
                  "_refResourceCollection": "managed/role",
                  "_refResourceId": "role-2",
                  "_refResourceRev": "00000000869a4909",
                  "_ref": "managed/role/role-2",
                  "_refProperties": {
                      "_id": "fda3bada-82b7-439f-8839-afc231fdd672",
                      "_rev": "10aaf9922670e7b31719067e32592ece2632beaf97585dc73660450c42a7dc89"
                  }
              },
              {
                  "_id": "07389948-8ae0-45fb-ac26-e0ee37c9d152",
                  "_rev": "598db7d3ac72dc4c65b8b3539cdba1c9507947dacc3cdaace7beeb101dc8b118",
                  "name": "role-3",
                  "description": "Role 3",
                  "members": [
                      {
                          "_ref": "managed/user/bob",
                          "_refResourceCollection": "managed/user",
                          "_refResourceId": "bob",
                          "_refProperties": {
                              "_id": "07389948-8ae0-45fb-ac26-e0ee37c9d152",
                              "_rev": "598db7d3ac72dc4c65b8b3539cdba1c9507947dacc3cdaace7beeb101dc8b118"
                          }
                      }
                  ],
                  "_refResourceCollection": "managed/role",
                  "_refResourceId": "role-3",
                  "_refResourceRev": "000000006e404730",
                  "_ref": "managed/role/role-3",
                  "_refProperties": {
                      "_id": "07389948-8ae0-45fb-ac26-e0ee37c9d152",
                      "_rev": "598db7d3ac72dc4c65b8b3539cdba1c9507947dacc3cdaace7beeb101dc8b118"
                  }
              }
          ],
          "resultCount": 3,
          "pagedResultsCookie": "eyAiX2lkIiA6ICJyb2w=",
          "totalPagedResultsPolicy": "NONE",
          "totalPagedResults": -1,
          "remainingPagedResults": -1
      }
      

      3. We can expand the relationships for the referred to objects. For example, querying user bobs roles allows us to do relationship expansion on the role objects that we receive.

      curl --location --request GET 'http://localhost:8080/openidm/managed/user/bob/roles?_queryFilter=_ref%20eq%20%22managed/role/role-2%22&_fields=/,/members' \
      --header 'X-OpenIDM-Username: openidm-admin' \
      --header 'X-OpenIDM-Password: openidm-admin' \
      --header 'Content-Type: application/json' 
      
      {
          "result": [
              {
                  "_id": "fda3bada-82b7-439f-8839-afc231fdd672",
                  "_rev": "10aaf9922670e7b31719067e32592ece2632beaf97585dc73660450c42a7dc89",
                  "name": "role-2",
                  "description": "Role 2",
                  "members": [
                      {
                          "_ref": "managed/user/bob",
                          "_refResourceCollection": "managed/user",
                          "_refResourceId": "bob",
                          "_refProperties": {
                              "_id": "fda3bada-82b7-439f-8839-afc231fdd672",
                              "_rev": "10aaf9922670e7b31719067e32592ece2632beaf97585dc73660450c42a7dc89"
                          }
                      }
                  ],
                  "_refResourceCollection": "managed/role",
                  "_refResourceId": "role-2",
                  "_refResourceRev": "00000000869a4909",
                  "_ref": "managed/role/role-2",
                  "_refProperties": {
                      "_id": "fda3bada-82b7-439f-8839-afc231fdd672",
                      "_rev": "10aaf9922670e7b31719067e32592ece2632beaf97585dc73660450c42a7dc89"
                  }
              }
          ],
          "resultCount": 1,
          "pagedResultsCookie": null,
          "totalPagedResultsPolicy": "NONE",
          "totalPagedResults": -1,
          "remainingPagedResults": -1
      }
      

      Those are the basic operations of the relationship endpoint that IDM invented for JDBC. In order to read and query high cardinality relationships, similar functionality needs to exist in rest2ldap as well.

       

      What is needed from DS

      Proposed Rest2ldap Reference Endpoints

      In order for IDM to implement the above endpoints, it will be necessary to implement forms of the above endpoints in rest2ldap. These are some proposals on what these rest2ldap endpoints could look like using the user/roles rest2ldap setup in this repo (https://stash.forgerock.org/users/jason/repos/relationship-perf/browse)

      It is worth noting in that example users and roles are generic objects, so their content is stored in a "fullObject" json blob. You can ignore this detail, but just know that is the content of the roles and users.

      Create, Update, Delete

      These endpoints are used to manage references on an origin resource. IDM implements these endpoints currently by patching the origin resource, these don't necessarily need to be implemented as part of this effort. These endpoints were implemented in IDM because with the JDBC repo relationships are their own objects.

      If this functionality was implemented the requests would look like this:

      PUT v1/managed/user/bob/roles/someRefId?_fields=/,/_refObject,/members
      
      {
        "_id" : "someRefId",
        "_refResourceId" : "adminRole" // note this is the id of the referred-to resource, this conflicts with the id of the reference, so it needs to be returned with some other field name than _id.
        "fullobject" : {
          "name" : "Admin Role",
          "description" : "Admin Role Description"
        },
        "members" : [ ... ],
        "_schema" : "managed_role",
        "_refObject" : {
          "_id" : "someRefId",
          "key" : "value"
        }
      }
      

      That request would add a reference to the managed/user/bob object. The update would be the same request but update the existing reference. Delete would also be the same, but delete the reference and be a DELETE request.

      Read

      Read is meant to read an individual reference. The id of the reference is the id of the _refObject. This endpoint will return the _refObject as well as the content of the referred-to role.

      GET v1/managed/user/bob/roles/someRefId?_fields=/,/_refObject,/members
      
      {
        "_id" : "someRefId",
        "_refResourceId" : "adminRole" // note this is the id of the referred-to resource, this conflicts with the id of the reference, so it needs to be returned with some other field name than _id.
        "fullobject" : {
          "name" : "Admin Role",
          "description" : "Admin Role Description"
        },
        "members" : [ ... ],
        "_schema" : "managed_role",
        "_refObject" : {
          "_id" : "someRefId",
          "key" : "value"
        }
      }
      

      Query

      Query is meant to query all the references for an origin resource. This endpoint will return the _refObject as well as the content of the referred-to roles. Query needs to support sortkeys, paging, and query filtering on the referred-to resource and the _refObject. It is important to note this query is NOT just GET v1/managed/roles?_queryFilter=members/name+eq+'bob'. That query will only give you one entry in the response per role, but bob could have the same role assigned 2 or more times. This distinction was discussed in slack (https://forgerock.slack.com/archives/CQHD1NJ7L/p1612133919085700)

      GET v1/managed/user/bob/roles?_queryFilter=true&_fields=/,/_refObject,/members
      
      {
        "results" : [
          {
             "_id" : "someRefId",
             "_refResourceId" : "adminRole" // note this is the id of the referred-to resource, this conflicts with the id of the reference, so it needs to be returned with some other field name than _id.
            "fullobject" : {
              "name" : "Admin Role",
              "description" : "Admin Role Description"
            },
            "_schema" : "managed_role",
            "_refObject" : {
              "_id" : "someRefId",
              "key" : "value"
            }
          }
        ],
        "resultCount": 1,
        "pagedResultsCookie": null,
        "totalPagedResultsPolicy": "NONE",
        "totalPagedResults": -1,
        "remainingPagedResults": -1
      }
      

      Note that whatever gets returned by rest2ldap will need to be translated by IDM to the format of the IDM endpoints in the first section. Since IDM has to do this translation, we need that content to come back from DS so we can do the translation. The rest2ldap endpoint proposals should contain all the information IDM needs to do the translation.

        Attachments

          Issue Links

            Activity

              People

              matthew Matthew Swift
              jason Jason Lemay
              Votes:
              0 Vote for this issue
              Watchers:
              3 Start watching this issue

                Dates

                Created:
                Updated:
                Resolved: