Vytvoření vlastního resource adaptéru pro Sun Identity Manager


Tento článek popisuje, jakým způsobem je možné naprogramovat vlastní „Standard Resource Adapter“ pro produkt Sun Identity Manager, respektive Oracle Waveset 8.1. Při psaní jsem vycházel ze zkušeností, které jsem společně s kolegy nasbíral při programování „univerzálního SSH adaptéru pro unixové systémy (pro Linux, AIX, HP-UX, Solaris a Sambu)“ .

Adaptér (Resource Adapter) je jakýmsi rozhraním mezi identity managerem a externím systémem. Prostřednictvím adaptéru probíhá veškerá komunikace mezi IdM a tímto externím systémem. V Sun IdM jsou dva základní druhy adaptérů – „Standard Resource Adapter“ a „Active Sync-Enabled Adapter“. Jak jsem již zmiňoval, v tomto článku se budeme zabývat tvorbou standardního adaptéru.

Mezi základní činnosti standardního resource adaptéru patří zajištění:

  • spojení s externím systémem
  • vytváření, mazání a aktualizace uživatelských účtů
  • povolování/zakazování uživatelských účtů
  • správa objektů (např. skupiny uživatelů, organizační struktura, apod.)

Jak začít?

Vytvoříme java class, která bude potomkem třídy com.waveset.adapter.ResourceAdapterBase. Ve třídě je nutné definovat několik základních částí adaptéru:

Konstruktor
Náš adaptér má jeden konstruktor bez parametrů a další s parametry typu Resource a ObjectCache:

public SshUniversalResourceAdapter(Resource res, ObjectCache cache) {
   super(res, cache);
}

public SshUniversalResourceAdapter() {
   super();
}

Definice proměnné prototypeXml
Jak již název napovídá, obsah proměnné bude obsahovat řetězec ve formátu XML. Konkrétně se jedná o XML, které definuje atributy externího systému (např. připojovací údaje), atributy uživatelského účtu (mapování IdM atributů na atributy externího systému), šablony (jak bude definován název účtu) a objekty (např. organizace na externím systému).

static final String prototypeXml =
    "<Resource name='" + RESOURCE_NAME + "'\n" +
    "          class='" + CLASS + "'\n" +
    "          typeString='" + RESOURCE_TYPE + "'\n" +
    "          typeDisplayString='" + RESOURCE_TYPE + "'>\n" +
    "  <ResourceAttributes>\n" +

    //
    // Declare connection-related attributes
    //

    "    <ResourceAttribute name='" + RA_HOST + "'\n" +
    "                       displayName='" + RAMessages.RESATTR_HOST + "'\n" +
    "                       type='string'\n" +
    "                       multi='false'\n" +
    "                       description='" + RAMessages.RESATTR_HELP_313 + "'/>\n" +
....
"  </ResourceAttributes>\n" +
"  <Template>\n" +
    "    <AttrDef name='accountId'\n" +
    "             type='string' />\n" +
    "  </Template>\n" +
"  <AccountAttributeTypes>\n" +
    "    <AccountAttributeType name='accountId'\n" +
    "                          mapName='login'\n" +
    "                          mapType='string'\n" +
    "                          required='true'>\n" +
    "      <AttributeDefinitionRef>\n" +
    "        <ObjectRef type='AttributeDefinition'\n" +
    "                   name='accountId'/>\n" +
    "      </AttributeDefinitionRef>\n" +
    "    </AccountAttributeType>\n" +
...
"  </AccountAttributeTypes>\n" +
    "  <ObjectTypes>\n" +
    "    <ObjectType name='Group' nameKey='UI_RESOURCE_OBJECT_TYPE_GROUP' icon='group'>\n" +
    "      <ObjectClasses operator='AND'>\n" +
    "        <ObjectClass name='group'/>\n" +
    "      </ObjectClasses>\n" +
    "      <ObjectFeatures>\n" +
    "        <ObjectFeature name='create'/>\n" +
    "        <ObjectFeature name='update'/>\n" +
    "        <ObjectFeature name='delete'/>\n" +
    "        <ObjectFeature name='saveas'/>\n" +
    "      </ObjectFeatures>\n" +
    "      <ObjectAttributes idAttr='groupName' displayNameAttr='groupName' descriptionAttr='description'>\n" +
    "        <ObjectAttribute name='groupName' type='string'/>\n" +
    "        <ObjectAttribute name='gid' type='string'/>\n" +
    "        <ObjectAttribute name='users' type='string'/>\n" +
    "      </ObjectAttributes>\n" +
    "    </ObjectType>\n" +
    "  </ObjectTypes>\n" +

Nyní si stručně popišmě, co jednotlivé elementy prototypeXml znamenají:

  • ResourceAttribute
    • Definuje atribut koncového systému (jeho název, datový typ a nápovědu k němu).
  • AccountAttributeType
    • Definuje mapování atributu mezi koncovým systémem a IdM.
  • ObjectType
    • Definuje vlastní datové typy (např. skupina uživatelů)
  • ObjectFeatures
    • Specifikeje, které operace jsou např. se skupinou možné. Tedy např. v našem případě je možné vytvořit, aktualizovat, smazat nebo uložit jako.

Do prototypeXml je samozřejmě možné umístit lokalizované textové konstanty (např. názvy atributů, nápověda). V příkladu výše jsem použil konstantu “RAMessages.RESATTR_HOST”. Její hodnota je definována takto:

public interface SSHRAMessages {
	...
	 public static final String RESATTR_HOST = "com.waveset.adapter.bcv.SSHRAMessages:RESATTR_HOST";
	...
}

(Soubor SSHRAMessages je .property soubor.)

Při vývoji mě poměrně zaskočilo to, že pokud se změní prototypeXml a provede se deploy, změny se projeví až poté, co se definuje (připojí) nový koncový systém.


Definice tzv. “Resource metod”

Resource metody zajišťují přenešení a zápis informací z IdM do koncového systému. Počet metod, které musíme při psaní vlastního adaptéru přepsat, závisí na konkrétním použití adaptéru. Existuje několik kategorií metod: (basic, bulk operations, active sync a object management).

Zde je uveden příklad metod, které je možné přepsat (seznam není kompletní):

  • public String runCommand(String cmd, String scriptCommand)
  • public WSUser getUser(WSUser user)
  • protected void realCreate(WSUser user, WavesetResult result)
  • protected void realUpdate(WSUser user, WavesetResult result)
  • protected void realDelete(WSUser user, WavesetResult result)
  • protected void realEnable(WSUser user, WavesetResult result)
  • protected void realDisable(WSUser user, WavesetResult result)
  • public WavesetResult createObject(GenericObject object, Map options)

Výše zmíněné metody a další byly naším adaptérem přepsány. Adaptér komunikuje s koncovým systémem pomocí SSH. Připojení a komunikaci zajišťuje knihovna JSch.jar. Zde je ukázka kódu, který zajistil připojení ke koncovému systému:

@Override
    protected void startConnection() throws WavesetException {
        final String method = "startConnection";
        TRACE.entry1(method);
        try {
            JSch jsch = new JSch();

            this.sshSession = createSession(jsch);

            setHostHashCheckingIfNeeded(jsch, this.sshSession);

            setPasswordOrKey(jsch, this.sshSession);

            this.sshSession.connect(CONNECTION_TIMEOUT);

        } catch (Exception e) {
            String errMsg = "Error connecting to server";

            TRACE.info1(method, errMsg);
            TRACE.caught1(method, e);

            throw new WavesetException(errMsg, Severity.ERROR, e);
        }
        TRACE.exit1(method);
    }

private Session createSession(JSch jsch) throws JSchException {
        final String method = "createSession";
        TRACE.entry1(method);

        Session result = jsch.getSession(
                getOptionalStringResAttrVal(RA_USER),
                getOptionalStringResAttrVal(RA_HOST),
                Integer.parseInt(getOptionalStringResAttrVal(RA_PORT))
        );

        TRACE.exit1(method);
        return result;
    }

protected void setPassword(Session session) throws WavesetException {
        final String method = "setPassword";
        TRACE.entry1(method);

        EncryptedData userPassword = null;

        Object passwordObject = getOptionalResAttrVal(RA_PASSWORD);
        if (passwordObject instanceof EncryptedData) {
            userPassword = (EncryptedData) passwordObject;
        } else {
            userPassword = new EncryptedData();
            userPassword.fromString((String) passwordObject);
        }

        if (userPassword == null) {
            throw new IllegalArgumentException("User password is null.");
        }

        session.setPassword(userPassword.decryptToString());

        TRACE.exit1(method);
    }

    protected void setKey(JSch jsch, String privateKey) throws JSchException {
        final String method = "setKey";
        TRACE.entry1(method);

        String user = getOptionalStringResAttrVal(RA_USER);
        EncryptedData keyPassword = (EncryptedData) getOptionalResAttrVal(RA_PRIVKEY_PASSWORD);

        if (keyPassword == null) {
            jsch.addIdentity(user, privateKey.getBytes(), null, null);
        } else {
            TRACE.info1(method, "Use private key - with key password");

            jsch.addIdentity(
                    user, privateKey.getBytes(),
                    null, keyPassword.decryptToString().getBytes()
            );
        }

        TRACE.exit1(method);
    }

private void setPasswordOrKey(JSch jsch, Session session) throws JSchException, WavesetException {
        //Getting private key from attribute
        String privateKey = getMultiValueString(_resource.getResourceAttribute(RA_PRIVKEY));

        if (privateKey != null && !privateKey.isEmpty()) {
            setKey(jsch, privateKey);

        } else {
            setPassword(session);
        }
    }

private void setHostHashCheckingIfNeeded(JSch jsch, Session session) {
        final String method = "setHostHashCheckingIfNeeded";
        TRACE.entry1(method);

        String hostKeyFingerPrint = getOptionalStringResAttrVal(RA_HOSTKEY);

        if (hostKeyFingerPrint != null && !hostKeyFingerPrint.isEmpty()) {

            HostFingerPrints fingerPrints = new HostFingerPrints(jsch);
            session.setConfig("StrictHostKeyChecking", "yes");
            fingerPrints.addFingerPrint(hostKeyFingerPrint);
            jsch.setHostKeyRepository(fingerPrints);

        } else {
            session.setConfig("StrictHostKeyChecking", "no");
        }

        TRACE.exit1(method);
    }

Přidání adaptéru do Identity Manageru

(Jedná se o návod pro NetBeans s přidaným pluginem pro vývoj IdM)

  • Zkompilujeme vytvářený adaptér do knihovny (v našem případě sshuniversalra.jar)
  • Zkopírovat knihovnu sshuniversalra.jar do adresáře /idm/web-inf/lib/:
    • přímo na aplikační server, kde je aplikace nasazena
      • v tomto případě bude nutné provést restart aplikačního serveru
    • do adresáře, ze kterého se provádí build
      • bude třeba upravit soubor /build-config.properties, ukázka:

        CLASSPATH=${custom.classes.dir}:${custom.lib.dir}/activation.jar:${custom.lib.dir}/jms.jar:${custom.lib.dir}/mail.jar:${lib.dir}/idm.jar:${lib.dir}/idmadapter.jar:${lib.dir}/idmauditor.jar:${lib.dir}/idmclient.jar:${lib.dir}/idmcommon.jar:${lib.dir}/idmformui.jar:${lib.dir}/idmspe.jar:${lib.dir}/j2ee.jar:${custom.lib.dir}/javacsv.jar:${custom.lib.dir}/jsch-0.1.42.jar:${custom.lib.dir}/sshuniversalra.jar
      • build aplikace
      • deploy aplikace

Přidání adaptéru v admin rozhraní

  • Resources › Configure Types
  • Add Custom Resource Adapter
  • Zadat název třídy (např. com.waveset.adapter.bcv.SshUniversalResourceAdapter)
  • Uložit


Kompletní a oficiální návod jak psát vlastní Resource Adapter naleznete na stránkách http://download.oracle.com/docs/cd/E19225-01/821-0093/ahuiy/index.html


V současné době je adaptér plně otestován na RHEL, Sambě 2.x a HP-UX trusted i non trusted. Vývoj probíhal ve dvou oddělených částech. První častí bylo napsání scriptů pro unixové systémy, druhou byl samotný vývoj adaptéru pro Sun IdM. Po vývoji obou částí došlo k jejich integraci, po které následovalo důsledné testování. Výsledek hodnotíme pozitivně a splnil naše očekávání.