Skip to content

JDBC

Le connecteur Scripted-JDBC permet la synchronisation des données complexe entre RAC/M Identity et les bases de données relationnelles utilisant le pilote JDBC.

Le connecteur a été testé avec SQL Server(2017, 2019, 2020), Azure SQL, Oracle, MySQL de la solution.

Jar

Cette configuration est basée sur le connecteur ICF Scripted SQL - 1.1.1.0.

Configuration

Les paramètres suivants sont nécessaires:

ConfigurationDescriptionExemple
Name QuotingDéfinit le style de guillemets utilisé pour les noms d'entités dans les scripts.", ', ou aucun
Reload Script on ExecutionIndique si le script doit être rechargé à chaque exécution.true ou false
Delete Script FilenameChemin du fichier script utilisé pour supprimer des enregistrements de la base de données.C:\Program Files\Okiok Data\RACM Identity\ICF\scripts 1.1.0.0\racm\Update.groovy
Search Script FilenameChemin du fichier script utilisé pour rechercher des enregistrements dans la base de données.C:\Program Files\Okiok Data\RACM Identity\ICF\scripts 1.1.0.0\racm\Search.groovy
Update Script FilenameChemin du fichier script utilisé pour mettre à jour des enregistrements dans la base de données.C:\Program Files\Okiok Data\RACM Identity\ICF\scripts 1.1.0.0\racm\Update.groovy
Schema Script FilenameChemin du fichier script utilisé pour initialiser ou valider le schéma de la base de données.C:\Program Files\Okiok Data\RACM Identity\ICF\scripts 1.1.0.0\racm\Schema.groovy
Test Script FilenameChemin du fichier script utilisé pour tester la connectivité ou une fonctionnalité spécifique.C:\Program Files\Okiok Data\RACM Identity\ICF\scripts 1.1.0.0\racm\TestSecurity.groovy
HostAdresse ou nom de domaine du serveur de base de données.localhost
PortNuméro de port utilisé pour se connecter au serveur de base de données.1433
UserNom d'utilisateur pour la connexion au serveur. Utilisez un compte dédié restreint aux tables LOGIN et LOGIN_ROLE, et non un compte administrateur intégré tel que sa.racm_connector
User PasswordMot de passe pour la connexion au serveur.Password123
DatabaseNom de la base de données à laquelle se connecter.racm_demo_config
JDBC DriverNom de la classe Java implémentant le pilote JDBC.com.microsoft.sqlserver.jdbc.SQLServerDriver
JDBC Connection URLURL utilisée pour la connexion.jdbc:sqlserver://ip:1433;databaseName=RACM_PROD_DATA

Configuration avancée

ConfigurationDescriptionExemple
Name QuotingDéfinit le style de guillemets utilisé pour les noms d'entités dans les scripts.", ', ou aucun
Reload Script on ExecutionIndique si le script doit être rechargé à chaque exécution.true ou false
Authenticate Script FilenameChemin du fichier script utilisé pour authentifier les utilisateurs ou les sessions.
Clear Text Password to ScriptSpécifie si les mots de passe en clair doivent être transmis au script.true ou false
autoCommitOption permettant d'activer ou de désactiver la validation automatique des transactions.true ou false
Scripting LanguageLangage utilisé pour les scripts.Groovy
Sync Script FilenameChemin du fichier script utilisé pour synchroniser les données entre différents systèmes.
Enable Writing Empty StringOption pour autoriser ou interdire l'écriture de chaînes vides dans la base de données.true ou false
Rethrow All SQLExceptionsOption permettant de relancer toutes les exceptions SQL capturées pour le débogage ou la journalisation.true ou false
Native TimestampsOption pour utiliser les types natifs de la base de données pour les horodatages, au lieu de conversions applicatives.true ou false
All NativeOption pour imposer l'utilisation exclusive des types natifs de la base de données pour tous les champs pris en charge.true ou false
Validate Connection QueryRequête SQL utilisée pour valider la connexion à la base de données.SELECT 1
Datasource PathChemin JNDI ou chemin direct vers la configuration de la source de données.
Initial JNDI PropertiesPropriétés spécifiques nécessaires pour initialiser le contexte JNDI de la source de données.

Note

Pour Azure SQL : Le seul driver disponible est com.microsoft.sqlserver.jdbc.SQLServerDriver.

Certificat

Un certificat pourrait être nécessaire en mode SSL.

Note

Au niveau de l'URL JDBC pour Microsoft Azure, le paramètre encrypt doit être présent afin de gérer le chiffrement des connexions SQL :

  • encrypt=false : Désactive le chiffrement, ce qui exclut l'utilisation d'un certificat. Cette configuration n'est pas recommandée pour des environnements de production.
  • encrypt=true : Active le chiffrement, ce qui inclut l'utilisation d'un certificat. Dans ce cas, le certificat SQL correspondant doit être présent dans le truststore de l'ICF (Identity Connector Framework).

Ajoutez le certificat au truststore de l'ICF.

Redémarrage

Attention

Lorsque vous effectuez un changement dans le truststore par défaut de l'ICF, un redémarrage du service RAC/M est obligatoire.

Test

Vous pouvez tester le connecteur Script sql en appuyant sur le bouton TEST. Par contre, une implémentation du Test Script doit être réalisée.

Importation

Les schémas définis dans Schema Script Filename seront disponibles pour la Colonne de la table source avec la primitive ModuleICFImportData dans les classes d'objets sous le connecteur ICF choisi.

  • _ACCOUNT_ est la variable par défaut d'ICF pour représenter les comptes définis dans le schéma.
  • _GROUP_ est la variable par défaut d'ICF pour représenter les groupes définis dans le schéma.

Mappage de comptes

  • ICF Connecteur -> Table source _ACCOUNT_ -> table cible APPLICATION_ACCOUNT_IMPORT.
Colonne de la table cibleColonne de la table sourceFormatage (Constante)Unique
ACCOUNT_NAME_NAME_
EINGIA_ID
EMAILEMAIL
EXT_APPLICATION_IDRACM
EXTRA1LANGUAGE
EXTRA2
FULL_NAMEFULL_NAME
GUID_GUID_
STATUSActif

Mappage de groupes

Chargement des groupes avec ModuleICFImportData.

  • ICF Connecteur -> Table source _GROUP_ -> table cible PROFILE_IMPORT.
Colonne RACMAttribut du système cibleFormatage (Constante)
GUID_UID_
EXT_PROFILE_ID_UID_
PROFIL_NAMEDESCRIPTION
EXT_APPLICATION_IDRACM

Mappage de membres

Chargement des membres avec ModuleICFImportData.

  • ICF Connecteur -> Table source _ROLE_MEMBERS_ -> table cible PROFILE_IMPORT.
Colonne RACMAttribut du système cibleFormatage (Constante)
ACCOUNT_NAMELOGIN
EXT_PROFILE_IDLOGIN_ROLE_ID
GUIDLOGIN_ROLE_ID
IDENTIFIER1_UID_
EXT_APPLICATION_IDRACM

Matérialisation

Dans l'actif une configuration doit être faite pour activer le provisionnement.

Ensuite, des modules doivent être configurés pour traiter les activations, modifications, créations, terminaisons et désactivations.
Le traitement de ces modules correspond à la matérialisation, qui consiste à créer les données nécessaires pour que le provisionnement puisse être traité.

  • La matérialisation est effectuée dans la table APPLICATION_ACCOUNT_IMPORT.
  • SYNC_STATUS sera défini à 0 lorsque le compte sera matérialisé et en attente du provisionnement.

Note

Si les informations ne sont pas disponibles dans le mapping de base ou si une jointure est nécessaire pour récupérer les données, un module SQL devrait être créé pour mettre à jour notre matérialisation.

Activer

Configurer une primitive ModuleModifyAccountFromAccountMng

  • Matérialiser un ACTIVATE.
  • Table source APPLICATION_ACCOUNT -> table cible APPLICATION_ACCOUNT_IMPORT.
Colonne de la table cibleColonne de la table sourceFormatage (Constante)Unique
ACCOUNT_NAMEACCOUNT_NAMEX
EINEIN
EMAILEMAIL
EXT_APPLICATION_IDRACM
EXTRA1EXTRA1
EXTRA2update
FULL_NAMEFULL_NAME
STATUSActif

Modifier

Configurer une primitive ModuleModifyAccountFromAccountMng

  • Matérialiser un MODIFY
  • Table source APPLICATION_ACCOUNT -> table cible APPLICATION_ACCOUNT_IMPORT.
Colonne de la table cibleColonne de la table sourceFormatage (Constante)Unique
ACCOUNT_NAMEACCOUNT_NAMEX
EINEIN
EMAILEMAIL
EXT_APPLICATION_IDRACM
EXTRA1EXTRA1
EXTRA2update
FULL_NAMEFULL_NAME
STATUSActif

Création

Configurer une primitive ModuleCreateAccountFromAccountMng

  • Table source APPLICATION_ACCOUNT -> table cible APPLICATION_ACCOUNT_IMPORT
  • ${ACCOUNT_NAME} sera résolu par la politique de création de compte.
    • EIN représentera le GIA_ID dans notre provisionnement.
    • Si une politique de mot de passe a été créée.
      • Un mot de passe sera généré même si le mapping n'est pas présent dans les mappings.
Colonne de la table cibleColonne de la table sourceFormatage (Constante)Unique
ACCOUNT_NAME${ACCOUNT_NAME}X
EINEIN
EMAILEMAIL
EXT_APPLICATION_IDRACM
EXTRA1EXTRA1
EXTRA2create
FULL_NAMEFULL_NAME
STATUSActif

Terminer

Configurer une primitive ModuleModifyAccountFromAccountMng

  • Matérialiser un TERMINATE
  • Table source APPLICATION_ACCOUNT -> table cible APPLICATION_ACCOUNT_IMPORT
Colonne de la table cibleColonne de la table sourceFormatage (Constante)Unique
ACCOUNT_NAMEACCOUNT_NAMEX
EXT_APPLICATION_IDRACM
FULL_NAMEFULL_NAME
EXTRA2terminate
STATUSTerminé

Désactiver

Configurer une primitive ModuleModifyAccountFromAccountMng

  • Matérialiser un DEACTIVATE
  • Table source APPLICATION_ACCOUNT -> table cible APPLICATION_ACCOUNT_IMPORT
Colonne de la table cibleColonne de la table sourceFormatage (Constante)Unique
ACCOUNT_NAMEACCOUNT_NAMEX
EINEIN
EXT_APPLICATION_IDRACM
EXTRA1EXTRA1
EXTRA2deactivate
FULL_NAMEFULL_NAME
STATUSDésactivé

Provisionnement

Les variables disponibles sont définies dans le schéma.

  • SYNC_STATUS sera mis à 1 quand le compte matérialisé sera provisionné.
  • L'attribut du système cible est défini dans le schéma de l'application cible.

Note

La variable action dans le script Groovy ne semble pas toujours être correctement initialisée. Une variable ACTION_PATCH pourrait être créée pour adresser le problème et transmettre les actions à traiter au script Groovy.

Provisionnement de compte

Configurer une primitive ModuleICFAccountProvisionning pour envoyer la matérialisation des comptes vers l'application cible.

  • Table source APPLICATION_ACCOUNT_IMPORT -> Connecteur ICF --> cible
Colonne RACMAttribut du système cibleUnique
ACCOUNT_NAMELOGINX
EINGIA_ID
EXTRA1LANGUAGE
EXTRA2ACTION_PATCH
FULL_NAMEFULL_NAME
PASSWORDPASSWORD
STATUSSTATUS

Provisionnement de groupe

Configurer une primitive ModuleICFProfileProvisionning pour envoyer la matérialisation des membres vers l'application cible.

  • Table source PROFILE_IMPORT -> Connecteur ICF --> _GROUP_

Note

Nous n'avons pas besoin de matérialiser les groupes car seulement les membres sont gérés par RAC/M.

Colonne RACMAttribut du système cibleUnique
ACCOUNT_NAMELOGINX
EXT_PROFILE_IDLOGIN_ROLE_ID

Groovy

Un fichier Groovy sera appelé par le connecteur ICF lorsqu'il exécute une action spécifique définie dans la configuration du connecteur ICF.
Tous les mappings seront disponibles dans le script Groovy sous les noms définis dans le schéma Groovy.
Nous pourrons accéder à ces variables et exécuter une requête SQL qui sera envoyée à la base de données cible.

Search Script Filename

Lorsqu'on effectue une lecture pour récupérer les informations d'une table, ce script Groovy est exécuté.

groovy
import groovy.sql.Sql

import java.util.logging.Logger

Logger logger = Logger.getLogger("")
logger.info("Entering Search script")


def sql = new Sql(connection)
def result = []
switch (objectClass) {
case "__ACCOUNT__":
query = """
SELECT  login_id, login, full_name, language, email  from LOGIN 
"""
logger.info("Executing query : " + query)
    result.add(__UID__: it.USER_ID, __NAME__: it.USER_NAME, LOGIN: it.LOGIN, FULL_NAME: it.FULL_NAME,
        LANGUAGE: it.LANGUAGE, EMAIL: it.EMAIL)
    
break
case "__GROUP__":
query = """
SELECT LOGIN_ROLE_ID, DESCRIPTION,LOGIN_ROLE_ID, DESCRIPTION from LOGIN_ROLE  
"""
logger.info("Executing query : " + query)
sql.eachRow(query, { result.add(__UID__: it.ROLE_ID, __NAME__: it.ROLE_NAME, ROLE_ID: it.ROLE_ID, ROLE_NAME: it.ROLE_NAME) })
break
case "ROLE_MEMBERS":
query = """
SELECT  login_id ,login, LOGIN_ROLE_ID from LOGIN
"""
logger.info("Executing query : " + query)
sql.eachRow(query, { result.add(__UID__: it.LOGIN_ID, __NAME__: it.LOGIN, LOGIN_ROLE_ID: it.LOGIN_ROLE_ID) })
break
default:
result
}

return result

Update Script Filename

When a change needs to be sent from RAC\M to the target, this script is executed.
It will check the action and action_patch variables to execute a specific section of the case (e.g., activate, delete, update, etc.).
In the connector configuration, it would have been possible to point to different files depending on the action performed by the ICF connector.

Note

La fonction hashPassword à la fin de ce script est volontairement laissée à l'état de pseudocode. Le stockage des mots de passe est propre à chaque application cible : implémentez-la avec le schéma exact utilisé par cette application pour valider les mots de passe, en vous appuyant sur une fonction de hachage de mot de passe lente et adaptative (Argon2id, scrypt, bcrypt ou PBKDF2-HMAC-SHA-256) plutôt que sur un simple condensat comme SHA-256 ou SHA-512.

groovy
package racm

import org.identityconnectors.common.logging.Log
import org.identityconnectors.framework.common.objects.Uid
import groovy.sql.Sql
import java.security.SecureRandom
import org.identityconnectors.common.security.SecurityUtil
import org.apache.commons.codec.binary.Base64

def log = log as Log

def LOGIN = attributes.get('LOGIN')
def LOGIN_ROLE_ID = attributes.get('LOGIN_ROLE_ID')
def FULL_NAME = attributes.get('FULL_NAME')
def PASSWORD = attributes.get('PASSWORD')
def LANGUAGE = attributes.get('LANGUAGE')
def EMAIL = attributes.get('EMAIL')
def GIA_ID = attributes.get('GIA_ID')
def STATUS = attributes.get('STATUS')
def ACTION_PATCH = attributes.get('ACTION_PATCH')

log.info("----- Update - login - Entering {0} script -----", action)
log.info("Entering {0} LOGIN", LOGIN)
log.info("Entering {0} LOGIN_ROLE_ID", LOGIN_ROLE_ID)
log.info("Entering {0} FULL_NAME", FULL_NAME)
log.info("Entering {0} LANGUAGE", LANGUAGE)
log.info("Entering {0} EMAIL", EMAIL)
log.info("Entering {0} GIA_ID", GIA_ID)
log.info("Entering {0} STATUS", STATUS)
log.info("Entering {0} ACTION_PATCH", ACTION_PATCH)
log.info("Entering {0} object class", objectClass)

def sql = new Sql(connection)
log.info("Connected to database, script update racm login")

if (toString(LOGIN).trim().equals('')) {
    log.info("LOGIN ne peut pas être vide")
    sql.close()
    return new Uid("")
}

switch (objectClass) {
    case "__GROUP__":
        log.info("Update login group")

        if ('ADD_ATTRIBUTE_VALUES'.equalsIgnoreCase(action)) {
            log.info("Add group member LOGIN_ROLE")

            // Toujours passer les valeurs en paramètres : les attributs d'identité sont
            // des entrées non fiables et peuvent légitimement contenir une apostrophe
            // (p. ex. O'Brien).
            updated = sql.executeUpdate(
                "UPDATE LOGIN SET LOGIN_ROLE_ID = ? WHERE LOGIN = ?",
                [toString(LOGIN_ROLE_ID), toString(LOGIN)])
        } else if ('REMOVE_ATTRIBUTE_VALUES'.equalsIgnoreCase(action)) {
            log.info("Remove group member LOGIN_ROLE")

            updated = sql.executeUpdate(
                "UPDATE LOGIN SET LOGIN_ROLE_ID = 3 WHERE LOGIN = ?",
                [toString(LOGIN)])
        } else {
            throw new Exception("Script not implemented for action: " + action + " LOGIN: " + toString(LOGIN_ROLE_ID).trim())
        }
        sql.commit()
        sql.close()
        return new Uid(LOGIN_ROLE_ID[0])

    case "__ACCOUNT__":
        log.info("Modify account")

        if ('TERMINATE'.equalsIgnoreCase(action) || "terminate".equalsIgnoreCase(ACTION_PATCH)) {
            log.info("Terminating login")

            sql.executeUpdate("DELETE FROM LOGIN WHERE LOGIN = ?", [toString(LOGIN)])

        } else if ('ACTIVATE'.equalsIgnoreCase(action) || "activate".equalsIgnoreCase(ACTION_PATCH)) {
            log.info("Activate login")

            def decryptPassword = SecurityUtil.decrypt(PASSWORD[0])
            def salt = generateSalt(16) // Génère un sel aléatoire de 128 bits (16 octets)
            def password = hashPassword(salt, decryptPassword)

            // Ne journalisez pas cette requête ni ses paramètres : ils contiennent le sel
            // et le hash du mot de passe, qui ne doivent pas se retrouver dans les logs.
            def count = sql.executeUpdate("""
                UPDATE LOGIN SET
                    PASSWORD_SALT = ?,
                    PASSWORD = ?,
                    FULL_NAME = ?,
                    GIA_ID = ?,
                    LANGUAGE = ?,
                    EMAIL = ?
                WHERE LOGIN = ?
            """, [salt, password, toString(FULL_NAME), toString(GIA_ID),
                  toString(LANGUAGE), toString(EMAIL), toString(LOGIN)])

            if (count == 0) {
                sql.executeUpdate("""
                    INSERT INTO [dbo].[LOGIN]
                    ([LOGIN], [PASSWORD_SALT], [PASSWORD], [LOGIN_ROLE_ID], [FULL_NAME], [GIA_ID], [LANGUAGE], [EMAIL])
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                """, [toString(LOGIN), salt, password, 3, toString(FULL_NAME),
                      toString(GIA_ID), toString(LANGUAGE), toString(EMAIL)])
            }

        } else if ('UPDATE'.equalsIgnoreCase(action) || "update".equalsIgnoreCase(ACTION_PATCH)) {
            log.info("Update login")

            sql.executeUpdate("""
                UPDATE LOGIN SET
                    FULL_NAME = ?,
                    GIA_ID = ?,
                    LANGUAGE = ?,
                    EMAIL = ?
                WHERE LOGIN = ?
            """, [toString(FULL_NAME), toString(GIA_ID), toString(LANGUAGE),
                  toString(EMAIL), toString(LOGIN)])

        } else {
            log.info("Updated login " + action)
        }
        sql.commit()
        sql.close()
        return new Uid(LOGIN[0])

    default:
        log.info("Script not implemented for {0}", objectClass)
        break
}

/**
 * Fonction pour générer un sel aléatoire de longueur donnée (ex: 16 octets pour 128 bits)
 */
def generateSalt(int length) {
    SecureRandom random = new SecureRandom()
    byte[] saltBytes = new byte[length]
    random.nextBytes(saltBytes)
    return Base64.encodeBase64String(saltBytes) // Encodé en Base64 pour stockage facile
}

/**
 * Fonction pour hacher un mot de passe avec un sel donné.
 *
 * PSEUDOCODE : cet exemple ne fournit volontairement aucune implémentation de
 * hachage, car l'algorithme doit correspondre à celui utilisé par l'application
 * cible pour valider les mots de passe de la table LOGIN. Implémentez-la avec une
 * fonction de hachage de mot de passe lente et adaptative (Argon2id, scrypt,
 * bcrypt ou PBKDF2-HMAC-SHA-256) et un facteur de travail ajusté à votre matériel.
 * N'utilisez jamais un simple condensat rapide (MD5, SHA-1, ou une seule passe de
 * SHA-256/SHA-512) : même salés, ceux-ci se cassent à grande vitesse sur GPU si la
 * table LOGIN venait à fuiter.
 */
def hashPassword(String salt, String password) {
    // Appelez la bibliothèque de hachage de mots de passe fournie avec
    // l'application cible, par exemple :
    //
    //   byte[] hash = Argon2id(password, salt,
    //                          memory: 64 MiB, iterations: 3, parallelism: 1, length: 32 bytes)
    //   -- ou --
    //   byte[] hash = PBKDF2_HMAC_SHA256(password, salt,
    //                                    iterations: 600000, length: 32 bytes)
    //
    //   return Base64.encodeBase64String(hash) // Encodé en Base64 pour stockage

    throw new UnsupportedOperationException(
        "Implémentez hashPassword() avec la fonction de hachage adaptative attendue par l'application cible")
}

Schema Script Filename

Définition du schéma du connecteur ICF par rapport au schéma de la base de données. Le mapping sera effectué dans chaque implémentation de script Groovy. Ce schéma sera utilisé également dans les interfaces de collecteurs.

groovy
package racm

import org.identityconnectors.framework.common.objects.AttributeInfo
import org.identityconnectors.framework.common.objects.AttributeInfoBuilder
import org.identityconnectors.framework.common.objects.ObjectClassInfo
import org.identityconnectors.framework.common.objects.ObjectClassInfoBuilder
import org.identityconnectors.common.security.GuardedString

import java.util.logging.Logger

Logger logger = Logger.getLogger("Initilazing logger")

// Declare the __ACCOUNT__ attributes
accAttrsInfo = new HashSet<AttributeInfo>()

accAttrsInfo.add(AttributeInfoBuilder.build("__UID__", String.class))
accAttrsInfo.add(AttributeInfoBuilder.build("__NAME__", String.class))
accAttrsInfo.add(AttributeInfoBuilder.build("LOGIN", String.class))
accAttrsInfo.add(AttributeInfoBuilder.build("LOGIN_ROLE_ID", String.class))
accAttrsInfo.add(AttributeInfoBuilder.build("FULL_NAME", String.class))
accAttrsInfo.add(AttributeInfoBuilder.build("LANGUAGE", String.class))
accAttrsInfo.add(AttributeInfoBuilder.build("PASSWORD", GuardedString.class))
accAttrsInfo.add(AttributeInfoBuilder.build("LANGUAGE", String.class))
accAttrsInfo.add(AttributeInfoBuilder.build("EMAIL", String.class))
accAttrsInfo.add(AttributeInfoBuilder.build("GIA_ID", String.class))
accAttrsInfo.add(AttributeInfoBuilder.build("STATUS", String.class))
accAttrsInfo.add(AttributeInfoBuilder.build("ACTION_PATCH", String.class))

// Create the __ACCOUNT__ Object class
final ObjectClassInfo ociAccount = new ObjectClassInfoBuilder().setType("__ACCOUNT__").addAllAttributeInfo(accAttrsInfo).build()
builder.defineObjectClass(ociAccount)

// Declare the __GROUP__ attributes
groupAttrsInfo = new HashSet<AttributeInfo>()

groupAttrsInfo.add(AttributeInfoBuilder.build("__UID__", String.class))
groupAttrsInfo.add(AttributeInfoBuilder.build("__NAME__", String.class))
groupAttrsInfo.add(AttributeInfoBuilder.build("LOGIN", String.class))
groupAttrsInfo.add(AttributeInfoBuilder.build("LOGIN_ROLE_ID", String.class))

// Create the __GROUP__ Object class
final ObjectClassInfo ociGroup = new ObjectClassInfoBuilder().setType("__GROUP__").addAllAttributeInfo(groupAttrsInfo).build()
builder.defineObjectClass(ociGroup)

// Declare the __ROLE_MEMBERS__ attributes
memberAttrsInfo = new HashSet<AttributeInfo>()

memberAttrsInfo.add(AttributeInfoBuilder.build("__UID__", String.class))
memberAttrsInfo.add(AttributeInfoBuilder.build("__NAME__", String.class))
memberAttrsInfo.add(AttributeInfoBuilder.build("LOGIN", String.class))
memberAttrsInfo.add(AttributeInfoBuilder.build("LOGIN_ROLE_ID", String.class))

// Create the __ROLE_MEMBERS__ Object class
final ObjectClassInfo ociMember = new ObjectClassInfoBuilder().setType("__ROLE_MEMBERS__").addAllAttributeInfo(memberAttrsInfo).build()
builder.defineObjectClass(ociMember)