Vývoj identity konektoru pro systém CzechIdM

Poslední dobou se moje pracovní povinnosti týkaly hlavně vývoje identity konektorů pro náš systém CzechIdM. Proto jsem se rozhodl, že napíši příspěvek, kde se pokusím letmo popsat problematiku Identity Managementu, Identity Connector Framework a vývoj konektorů v něm. Tento příspěvek zároveň volně navazuje na příspěvek mého kolegy Zdeňka Burdy o systému CzechIdM.

Úkolem Identity Managementu (IdM) je správa celého životní cyklu identit v různých systémech od vzniku identity, změn parametrů až po zánik identity. Zjednodušeně řešeno se jedná o systém, který spravuje uživatelské identity (kdo nebo co má přístup) a jejich vztah k jednotlivým aplikacím a datům (kam má přístup) a na základě jaké pravomoci získal přístupy (schváleno nadřízeným nebo dle role, organizačního zařazení atd.). Tyto informace jsou jednoduše auditovatelné a reportovatelné v čase, a to vše v souladu s bezpečnostními politikami. Aby to nebylo tak jednoduché, tak pod identitou si lze představit uživatelský účet, certifikát, reprezentace PC v doméně, ale i role a organizační strukturu. Každý druh identit mívá vlastní životní cyklus.

Výše popsané může vyvolávat dojem, že připojení koncových systémů k systému IdM musí vyžadovat jejich nemalé uzpůsobení pro podporu identity managementu. Opak je pravdou. Velkým přínosem IdM je snadnost připojení koncového systému bez nutnosti jeho přizpůsobení nebo dokonce i restartu. A jak na to? Využívají se obecnější přístupy mezi něž patří také tzv. identity konektory (Identity Connectors). A právě popisem vývoje jednoho takového konektoru v Identity Connector Frameworku pro náš systém CzechIdM se zabývá tento článek.

Nejprve si povíme, co jsou to konektory a jak se používají. Poté se dostaneme k samotnému vývoji konektoru v Javě. Zde si popíšeme jednotlivé části konektoru a jejich vývoj. Závěrem si ukážeme, jak napsat jednoduchou klientskou aplikaci na ověření požadované funkcionality konektoru.

Co to jsou identity konektory a k čemu slouží

Identity konektory (dále jen konektory) jsou aplikační komponenty, prostřednictvím kterých se spravují uživatelské účty (a nemusí se jednat pouze o ně) na koncových systémech. Tvoří k nim jakousi fasádu, tj. poskytují jednotné rozhraní pro práci s nimi. Koncové systémy jsou v tomto případě všechny aplikace a systémy, které používají vlastní správu uživatelů, např. systémy pro sledování požadavků, HR systémy, mzdové systémy atd. Úkolem konektorů je poskytovat funkcionalitu pro připojení se k daným koncovým systémům a delegovat operace pro správu uživatelských účtů na tyto systémy.

Konektory obecně poskytují metody pro:

  • vytvoření nového uživatelského účtu, popř. skupiny, role, organizace apod.
  • editaci stávajícího uživatelského účtu
  • smazání uživatelského účtu
  • vyhledání uživatelského účtu
  • vyhledání všech účtů
  • vyhledání účtů dle vybraného filtru

Toto jsou základní operace, které nám konektory poskytují pro práci s uživatelskými účty a skupinami. Obecně mohou konektory pracovat i s jinými objekty než jen s účty a skupinami, ale primárně jsou určeny a optimalizovány právě pro ně.

Pokud chceme používat nějaký konektor v naší aplikaci, tak potřebujeme mít:

  1. Identity Connector Framework.
  2. Daný konektor samozřejmě (označován také jako bundle).

Na oficiálních stránkách projektu Identity Connectors je k dispozici několik konektorů pro různé koncové systémy (od Google Apps až po LDAP). Framework lze získat z SVN-ka na adrese https://svn.java.net/svn/identityconnectors~svn (je potřeba se předem zaregistrovat). Po checkoutu máme k dispozici zdrojové kódy frameworku (jak pro Javu, tak i pro .NET) a zdrojové kódy několika konektorů. Právě zdrojové kódy konektorů jsou k nezaplacení při psaní vlastního konektoru, protože dokumentace není mnoho.

Dále předpokládejme, že jsme provedli checkou trunku do adresáře ${connector_framework}. Potom zdrojové kódy konektorů jsou v adresáři ${connector_framework}/trunk/projects/bundles a framework pro Javu v adresáři ${connector_framework}/trunk/projects/framework/java. Zde jsou pouze zdrojové kódy, a proto je potřeba provézt build frameworku. Ten se provádí ANTem dle konfiguračního souboru ${connector_framework}/trunk/projects/framework/java/build.xml.

Popis Identity Connector Frameworku

Identity Connector Framework lze rozdělit na dvě části:

  1. Connector API – aplikace používají toto rozhraní pro volání implementovaných metod konektoru.
  2. Connector SPI – SPI deklaruje rozhraní, které implementují SPI vývojáři při tvorbě nových konektorů.

Na obrázku níže je struktura Identity Connector Frameworku. Zde lze vidět, že prostřednictvím SPI komunikuje konektor s koncovými systémy a API poskytuje rozhraní pro volání metod konektoru.

Connector API

Connector API poskytuje jednotné rozhraní pro volání metod konektorů bez ohledu na to, jaké metody konektor implementuje. SPI vývojářům usnadňuje práci tím, že jsou zde již implementovány některé API funkcionality, které mohou jednoduše použít (s minimem vlastní implementace). Z pohledu klienta konektoru není většinou potřeba pro využívání těchto vlastností dělat vůbec nic, maximálně je nakonfigurovat.

Mezi poskytované API vlastnosti patří například:

  • Connection pooling
  • Timeouty na prováděné operace – klienti konektoru pouze nastaví požadované timeouty. Vývojáři konektoru nemusí implementovat vůbec nic.
  • Velké množství vyhledávacích filtrů – vývojáři konektorů pouze implementují ty filtry, které daný koncový systém podporuje. O zbytek práce se postará framework.
  • Podpora skriptování v Groovy nebo Boo .NET – framework může spouštět skripty jak nad konektorem, tak i nad koncovým systémem (pokud to podporuje).

Connector SPI

Connector SPI tvoří sada několika rozhraní, které vývojáři konektorů musí implementovat, pokud požadují, aby konektor poskytoval danou operaci. Pro každou operaci existuje ve frameworku vlastní rozhraní.

Seznam SPI operačních rozhraní:

  • AuthenticateOp – poskytuje rozhraní metody pro přihlášení se ke koncovému systému, metoda ověřuje identitu objektu dle jeho uživatelského jména a hesla, navrací Uid daného objektu, pokud autentizace proběhla úspěšně.
  • CreateOp – poskytuje rozhraní metody pro vytváření nových objektů na koncovém systému.
  • DeleteOp – poskytuje rozhraní pro mazání objektů na koncovém systému.
  • ResolveUsernameOp – poskytuje rozhraní metody, která pro zadané uživatelské jméno objektu navrací jeho Uid (identifikátor).
  • SchemaOp – rozhraní metody, která poskytuje klientské aplikaci informace o atributech objektů koncového systému.
  • ScriptOnConnectorOp – rozhraní metody pro spuštění skriptů v prostředí konektoru.
  • ScriptOnResourceOp – poskytuje rozhraní metody, která spouští skripty na koncovém systému.
  • SearchOp<T> – rozhraní deklarující metody pro vyhledávání objektů na koncovém systému.
  • TestOp – rozhraní deklarující metodu testující spojení s koncovým systémem.
  • UpdateAttributeValuesOp – rozhraní pokročilejších metod editaci stávajících objektů koncového systému.
  • UpdateOp – deklaruje metodu pro editaci stávajícího objektu na koncovém systému.

Další zajímavým SPI rozhraním je PoolableConnector. V tomto rozhraní je deklarována metoda checkAlive, která ověřuje, zda je daná instance konektoru stále aktivní. Pokud instance aktivní již není (např. vypršel timeout na spojení s koncovým systémem), tak by měla vyhodit výjimku RuntimeException, což dává frameworku signál, že je potřeba vytvořit novou instanci konektoru. Jelikož se tato metoda volá často, tak by měla být doba jejího běhu co možná nejkratší.

Vývoj univerzálního SSH konektoru

Nyní se již pustíme do vývoje vlastního konektoru. Bude se jednat se o „univerzální SSH konektor“. Co se skrývá za tímto názvem?

  • SSH – pro připojení se ke koncovému systému (případně serveru, na kterém běží koncová aplikace) je použit protokol SSH2
  • univerzální – konektor je maximálně univerzální, protože metody konektoru volají pro jednotlivé operace správy uživatelských účtů a skupin (create, update, delete,..) skripty, které provádějí požadované změny na daném koncovém systému. Pokud chceme použít SSH univerzální konektor k připojení nějaké nové aplikace k CzechIdM, tak stačí pouze napsat ony skripty pro dané zařízení dle specifikovaného rozhraní. Data mezi konektorem a skripty se přenášejí ve formátu CSV nebo jako obyčejné texty.

Popis SSH konektoru:

  • Připojení ke koncovému systému zajištěno protokolem SSH2, autentizace uživatelským jménem a heslem, autentizace privátním klíčem, kontrola otisku veřejného klíče serveru.
  • Podporovány jsou všechny standardní operace pro správu uživatelských účtů a skupin – vytvoření, editace, smazání, vyhledání uživatelských účtů a skupin (rozhraní CreateOp, DeleteOp, UpdateOp,SearchOp<String>).
  • Implementována metoda test pro testování spojení se systémem (implementace rozhraní TestOp).
  • Implementována metoda schema poskytující defaultní popis atributů objektů na koncovém systému (rozhraní SchemaOp).

Implementované třídy (popř. rozhraní) SSH konektoru:

  • SSHConfiguration
  • SSHConnection
  • SSHUserFilterTranslator
  • SSHGroupFilterTranslator
  • SSHMessages
  • SSHConnector

Třída SSHConfiguration

Tato třída dědí od třídy AbstractConfiguration a zapouzdřuje konfiguraci konektoru. V tomto případě se v konfiguraci uvádějí údaje potřebné pro připojení se ke koncovému systému (uživatelské jméno, heslo, adresa koncového systému, port atd.), cesty k jednotlivým skriptům pro operace konektoru atd..Třída má defaultní konstruktor a pro každý konfigurační údaj jsou zde metody „set“ a „get“, přičemž metoda „get“ je anotována anotací @ConfigurationProperty. Níže je uveden příklad pro uživatelské jméno klienta, pod kterým se konektor připojuje na koncový systém. Anotace @ConfigurationProperty má několik možných parametrů.

  • order – určuje pořadí konfiguračního údaje ve výpisu všech konfiguračních údajů
  • displayMessageKey – slouží jako klíč k názvu konfiguračního údaje (tyto popisky jsou v property souboru Messages.properties a jsou načítány metodou getMessage)
  • helpMessageKey – popisek konfiguračního údaje, opět jako klíč do property souboru
  • required – určuje, jestli je daný údaj povinný
  • confidential – určuje, zde má být hodnota zašifrována
@ConfigurationProperty(order = 3,
	displayMessageKey = "SSH_USER_NAME",
	helpMessageKey = "SSH_USER_HELP",
	required = true)
public String getUsername() {
	return username;
}

Tato třída je také odpovědná za validaci vstupních hodnot. K tomuto účelu slouží metoda validate. Ta pouze kontroluje syntaktickou stránku konfiguračních údajů, neměla by ověřovat dostupnost zdrojů (např. připojení k databázi). Pokud zadané údaje nejsou „well-formed“, tam metoda vyhazuje výjimku typu RuntimeException.

Implementace rozhraní AbstractConfiguration je vyžadováno a musí ho implementovat všechny konektory.

/**
 * Validuje konfiguraci konektoru. Kontroluje, zda jsou nastaveny všechny potřebné parametry.
 * Implementace by měla pouze kontrolovat syntaktickou stránku, tjn. jestli jsou vsechny potřebné
 * parametry "well-formed". Neměla by se snažit ověřovat dostupnost zdrojů, např. připojovat se k nim.
 */
@Override
public void validate() {
	if (StringUtil.isBlank(getHost())) {
		throw new IllegalArgumentException("Hostname must be set.");
	}
	if (getPort() < 0 || getPort() >= 65535) {
		throw new IllegalArgumentException("Port must be in range between 1 to 65535.");
	}
	if (StringUtil.isBlank(getUsername())) {
		throw new IllegalArgumentException("Username must be specified.");
	}
	if (!getEscapeMode().equals(SSHMessages.SSH_ESCAPE_MODE_DOUBLED) && !getEscapeMode().equals(SSHMessages.SSH_ESCAPE_MODE_BACKSLASH)) {
		throw new IllegalArgumentException("Escape mode must be BACKSLASH or DOUBLED");
	}
}

Třída SSHConnection

Úkolem této třídy je spravovat spojení s koncovým systémem. Konstruktoru této třídy se předává instance konfigurační třídy SSHConfiguration.

Pro připojení ke koncovému systému slouží metoda startConnection, pro ukončení spojení slouží metoda dispose a pro testování spojení metoda test.

/**
 * Konstruktor třídy SSHConnection.
 *
 * @param cfg konfigurace, tj. instance třídy SSHConfiguration
 * @throws Exception
 */
public SSHConnection(SSHConfiguration cfg) throws Exception {
	if (cfg == null) {
		throw new Exception("Configuration not set.");
	}
	config = cfg;
} 

/**
 * Metoda pro vytvoření spojení s koncovým systémem. Pokud je uveden privátní klíč, tak
 * se implicitně použije pro autentizaci. Jinak se použije dvojice uživatelské jméno a
 * heslo. Pokud je uveden otisk veřejného klíče serveru, ke kterému se připojujeme, tak
 * se použije pro jeho verifikaci.
 *
 * @return Instance třídy Session.
 */
public Session startConnection() {
	String privateKey = asString(config.getPrivkey());
	try {
		if (!StringUtil.isBlank(privateKey)) {
			//Private key used for authentication
			log.info("Private key used for authentication.");
			createSSHConnectionWithPrivateKey(privateKey, config.getPrivkeyPassword());
		} else {
			//Authentication via password
			log.info("Authentication via password.");
			createSSHConnectionWithPassword(config.getPassword());
		}

		session.connect(SSHConfiguration.CONNECTION_TIMEOUT);
		log.info("Succesfull connection.");
	} catch (Exception ex){
		log.error("Connecting to server failed. {0}",ex.getMessage());
		throw new ConnectionFailedException("Connecting to server failed.");
	} finally {
		//clear user password
		session.setPassword("");
	}
	return session;
}

/**
 * Metoda pro ukončení spojení.
 * {@inheritDoc}
 */
public void dispose() {
	log.info("Dispose connection.");
	if (session != null) {
		session.disconnect();
	}
}

/**
 * Metoda testující navázané spojení.
 * {@inheritDoc}
 */
public void test() {
	config.validate();
	startConnection();
	dispose();
}

Třídy SSHUserFilterTranslator a SSHGroupFilterTranslator

Tyto třídy dědí z AbstractFilterTranslator<String> a slouží pro vytváření vyhledávacích dotazů. Stačí implementovat pouze ty metody třídy AbstractFilterTranslator podle nichž chceme objekty vyhledávat. V našem případě stačilo implementovat metodu createEqualsExpression (viz. níže).

/**
 * Metoda vytvoří dotaz (ve formě atributů pro příslušný skript koncového systému)
 * pro vyhledání uživatelského účtu dle specifikovaného uživatelského jména (prozatím).
 * Uživatelské jméno (instance třídy Name nebo Uid) je uloženo jako atribut v objektu
 * filter.
 */
@Override
protected String createEqualsExpression(EqualsFilter filter, boolean not) {
	if (not) {
		throw new UnsupportedOperationException("Not supported yet.");
	}

	String username = "";
	Attribute attrib = filter.getAttribute();
	if (attrib == null) {
		return null;
	}

	if (attrib.is(Name.NAME)) {
		username = ((Name)attrib).getNameValue();
	} else if (attrib.is(Uid.NAME)) {
		username = ((Uid)attrib).getUidValue();
	} else if (attrib.getValue() != null) {
		username = (String)attrib.getValue().get(0);
	}		

	String operationName = SSHMessages.SSH_GETUSER;
	String header = SSHMessages.SSH_HEADER_ACCOUNTID;         

        String scriptParams = String.format("%s\n%s\n%s\n", operationName, header, username);
	return scriptParams;
}

Tato metoda vytvoří vyhledávací dotaz. Při vyhledávání konkrétního uživatele se vytvoří instance daného filtru (metodou createFilterTranslator třídy SSHConnector), vytvoří se odpovídající dotaz, který se poté předá jako parametr query metodě executeQuery (také ve třídě SSHConnector), která se již pokusí uživatelský záznam najít.

/**
 * Metoda slouží pro spuštění dotazu nad objekty koncového systému.
 */
public void executeQuery(ObjectClass oclass, String query, ResultsHandler handler, OperationOptions options) {
	ConnectorObject object = null;
	if (query == null) {
		//Vylistovat vsechny objekty dane tridy.
		Session session = connection.startConnection();
		Iterator<Name> it = getAllObjectNames(oclass).iterator();
		Name m = null;
		String scriptParams;
		try {
			while (it.hasNext()) {
				m = it.next();
				scriptParams = createGetQuery(oclass, m.getNameValue());
				object = getConnectorObject(oclass, scriptParams, session);
				if (object != null) {
					handler.handle(object);
				}
			}
		} catch (ConnectorException ex) {
			throw new ConnectorException(ex.getMessage());
		} finally {
			if (session != null && session.isConnected()) {
				session.disconnect();
			}
		}
	} else {
		//Vylistovat pouze zaznam odpovidajici danemu dotazu (query).
		object = getConnectorObject(oclass, query, null);
		if (object != null) {
			handler.handle(object);
		}
	}
}

/**
 * Metoda navrací dle použité třídy objektů odpovídající filtr.
 */
public FilterTranslator<String> createFilterTranslator(ObjectClass oclass, OperationOptions options) {
	if (oclass.is(ObjectClass.ACCOUNT_NAME)) {
		return new SSHUserFilterTranslator();
	} else if (oclass.is(ObjectClass.GROUP_NAME)) {
		return new SSHGroupFilterTranslator();
	}
	return null;
}

Rozhraní SSHMessages

Rozhraní SSHMessages slouží pouze pro definování použitých konstant v celém konektoru.

Třída SSHConnector

Je hlavní a nejpodstatnější třídou konektoru. Je anotována anotací @ConnectorClass a implementuje interface org.identityconnectors.framework.spi.Connector a SPI rozhraní operací, které má konektor podporovat. Jejími členskými proměnnými jsou instance tříd SSHConnection a SSHConfiguration. Vyžadován je bezparametrický konstruktor.

Rozhraní org.identityconnectors.framework.spi.Connector deklaruje metody init(Configuration) a dispose, které jsou volány při každém spuštění operace nad koncovým systémem. Metoda init slouží pro získání konfigurace konektoru a pro navázání spojení, metoda dispose naopak spojení ukončuje. Spolu se třídou Configuration jsou to jediné třídy, které musejí být implementovány ve všech konektorech.

package org.identityconnectors.ssh;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.*;

import org.identityconnectors.common.StringUtil;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.objects.*;
import org.identityconnectors.framework.common.objects.AttributeInfo.Flags;
import org.identityconnectors.framework.common.objects.filter.FilterTranslator;
import org.identityconnectors.framework.spi.Configuration;
import org.identityconnectors.framework.spi.Connector;
import org.identityconnectors.framework.spi.ConnectorClass;
import org.identityconnectors.framework.spi.operations.*;
import org.identityconnectors.ssh.filters.SSHGroupFilterTranslator;
import org.identityconnectors.ssh.filters.SSHUserFilterTranslator;

import com.csvreader.CsvReader;
import com.csvreader.CsvWriter;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.Session;

/**
 * Třída implementující funkcionalitu poskytovanou SSH konektorem.
 *
 * @author Jaromír Mlejnek
 */
@ConnectorClass(displayNameKey="SSH_Universal_Connector",
		configurationClass = SSHConfiguration.class)
public class SSHConnector implements Connector, CreateOp, DeleteOp, SearchOp<String>,
	UpdateOp, SchemaOp, TestOp {

    private static Schema schema;
    private static final String ENCODING = "UTF-8";

    private SSHConfiguration config;
    private SSHConnection connection;    

    private List<String> multiValueAttribs; 

    //Logger
    Log log = Log.getLog(SSHConnector.class);

    /**
     * Implicitní konstruktor.
     */
    public SSHConnector() {
    }    

   /**
    * Metoda navracející konfiguraci.
    */
    public Configuration getConfiguration() {
        return this.config;
    }

    /**
     * Metoda pro načtení konfigurace a inicializaci spojení.
     */
    public void init(Configuration cfg) {
        config = (SSHConfiguration)cfg;
        try {
        	connection = new SSHConnection(config);
        } catch (Exception ex){
        	log.error("Exception during initialization.");
        	ex.printStackTrace();
        }

        if (config.getMultiValueAttributes() == null || config.getMultiValueAttributes().length == 0) {
        	multiValueAttribs = new ArrayList<String>();
        } else {
        	multiValueAttribs = Arrays.asList(config.getMultiValueAttributes());
        }
    }

    /**
     * Metoda pro ukončení spojení.
     */
    public void dispose() {
    	if (connection != null) {
    		connection.dispose();
    	}
    }

    /**
     * Metoda spouštějící test spojení. Pokud není spojení s koncovým systémem navázáno, tak
     * metoda vyhodí výjimku.
     */
    public void test() {
    	log.info("SSHConnector - test");
    	connection.test();
    } 

    .
    .
    .

Nemá cenu podrobně rozebírat implementaci všech metod, podívejme se tedy alespoň na metodu create. Jejími parametry jsou:

  • ObjectClass oclas – určuje, jestli se bude vytvářet uživatelský účet (ObjectClass.ACCOUNT) nebo skupina (ObjectClass.GROUP)
  • Set<Attribute> attrs – množina atributů (dvojice název atributu a jeho hodnota) nově vytvářeného objektu
  • OperationOptions options – případné další parametry (není vyžadováno)

Předpokládejme, že chceme vytvořit uživatelský účet (parametr ObjectClass oclas bude ObjectClass.ACCOUNT). Načteme si cestu k příslušnému skriptu pro vytvoření uživatele na koncovém systému (do proměnné pathToScript) a zavoláme metodu createOrUpdateUser. Ta projde předávanou množinu atributů a vytvoří vstupní parametry příslušného skriptu (ve formátu CSV). Pak spustí příslušný skript a předá mu dané CSV parametry. Skript vstup rozparsuje, vytvoří uživatelský účet a navrátí konektoru jedinečný identifikátor tohoto účtu (opět ve formátu CSV). Ten pak konektor deleguje volající aplikaci jako instanci třídy Uid (podtřída třídy Attribute, slouží jako jednoznačný identifikátor objektů).

/**
 * Metoda pro zakládání objektu daného typu (ACCOUNT nebo GROUP) na koncovém systému.
 */
public Uid create(ObjectClass oclass, Set<Attribute> attrs, OperationOptions options) {
	String operationName = "";
	String pathToScript = "";
        Uid returnUid = null;    	        

	if (oclass.is(ObjectClass.ACCOUNT_NAME)) {
		operationName = SSHMessages.SSH_CREATEUSER;
		pathToScript = config.getCreateUser();
		checkPathToScript(pathToScript, operationName);
		returnUid = createOrUpdateUser(operationName, pathToScript, attrs);

	} else if (oclass.is(ObjectClass.GROUP_NAME)) {
		operationName = SSHMessages.SSH_CREATEGROUP;
		pathToScript = config.getCreateGroup();
		checkPathToScript(pathToScript, operationName);
		returnUid = createOrUpdateGroup(operationName, pathToScript, attrs);
	}
        return returnUid;
}      

/**
 * Metoda provádí dle názvu operace, cesty ke skriptu a zadaných atributů příslušnou operaci s
 * uživatelským účtem.
 *
 * @param operationName název prováděné operace.
 * @param pathToScript cesta k danému skriptu.
 * @param attrs množina zadaných atributů.
 * @return Uid uživatelského účtu, který se vytvořil nebo měnil.
 */
private Uid createOrUpdateUser(String operationName, String pathToScript, Set<Attribute> attrs) {
	StringBuffer userHeader = new StringBuffer();
        List<String> dataForUserLine = new ArrayList<String>();

	Attribute attrib = null;
	Iterator<Attribute> it = attrs.iterator();
	while (it.hasNext()) {
		attrib = it.next();
		if (attrib.is(Name.NAME)) {
			String name = getName(attrib);
			userHeader.append(SSHMessages.SSH_HEADER_ACCOUNTID);
			userHeader.append(SSHConfiguration.DELIMITER);
			dataForUserLine.add(name);
		} else if (attrib.is(OperationalAttributes.PASSWORD_NAME)) {
	                userHeader.append(SSHMessages.SSH_HEADER_PASSWORD);
	                userHeader.append(SSHConfiguration.DELIMITER);
	                dataForUserLine.add(getPassword(attrib));
		} else {
			userHeader.append(attrib.getName());
    		        userHeader.append(SSHConfiguration.DELIMITER);
    		        dataForUserLine.add(getAttributeValue(attrib));
		}
	}
	//Odstranime delimiter na konci radku
	userHeader = removeLastChar(userHeader);

	String scriptParams = createCommandCSV(operationName, userHeader, dataForUserLine);
	String result = runCommand(pathToScript, scriptParams);
	String accountUid = "";
	try {
		CsvReader reader = CsvReader.parse(result);
		reader.setDelimiter(SSHConfiguration.DELIMITER);
		reader.setEscapeMode(getCsvReaderMode());
		reader.readHeaders();
		reader.readRecord();
		accountUid = reader.get(0);
	} catch (IOException ioExc) {
		log.error("Exception during read from CSV file. \nError: {0}", ioExc.getMessage());
	}
	if (StringUtil.isBlank(accountUid)) {
		return null;
	}
	return new Uid(accountUid);
}

Na podobném principu pracují i ostatní operace (metody update, delete atd.). Vždy se určí cesta k odpovídajícímu skriptu, sestaví se jeho vstupní parametry ve formátu CSV, které se mu následně předají, skript se provede a případný výstup je předán konektoru (opět jako CSV). Konektor výstup rozparsuje a dále dle potřeby zpracuje.

Posledním krokem, co musíme při vývoji konektoru udělat, je napsat ANT build skript build.xml, podle kterého se provede build konektoru. Není to nic těžkého, protože zde stačí pouze uvézt cestu ke konfiguračnímu souboru connector_build.xml (standardně se nachází v adresáří ${connector_framework}/trunk/projects/framework/java). V tomto souboru jsou uvedeny všechny potřebné cesty k třídám frameworku atd. Pokud tedy neproběhne build konektoru bez problému, tak je nejspíše chybně nastavená některá z cest v tomto souboru.

V našem případě obsahuje soubor build.xml následující řádky.

<project name="connector-ssh" default="all">
    <property name="framework.dir" value="../java"/>
    <import file="${framework.dir}/connector_build.xml"/>
</project>

Spolu s build skriptem je ještě potřeba vytvořit build.properties soubor, ve kterém je uvedena použitá verze franeworku a název konektoru. V našem případě:

MAJOR=1
MINOR=0
ConnectorBundle-FrameworkVersion=1.0
ConnectorBundle-Name=org.identityconnectors.ssh
connectorName=org.identityconnectors.ssh.SSHConnector

Nyní již můžeme provézt buidl konektoru ANTem.

Vzorový klient konektoru

Nyní, když již máme vytvořený náš SSH konektor, tak se podíváme, jakým způsobem ho můžeme použít. Ukážeme si, jak napsat jednoduchou aplikaci, která bude tento konektor používat pro správu uživatelů na systému Request Tracker.

Vytvoříme si obyčejný Java projekt, přidáme do něho knihovny connector-framework.jar a connector-framework-internal.jar a můžeme začít. Vytvoříme hlavní metodu main, do které zapíšeme následující kód.

try {
	// 1. Soubor s konektorem.
	File bundleDirectory = new File("/home/jarda/workspace/Connector_Client/bundles");
	URL db1Url = IOUtil.makeURL(bundleDirectory, "org.identityconnectors.ssh-1.0.956.jar");			

	// 2. Inicializace connector manageru, který udržuje seznam konektorů (přesněji informace o nich).
	ConnectorInfoManagerFactory factory = ConnectorInfoManagerFactory.getInstance();
	ConnectorInfoManager manager = factory.getLocalManager(db1Url);

	List<ConnectorInfo> connectorInfos = manager.getConnectorInfos();

	// 3. Vypsání dostupných konektorů.
	System.out.println("Available connector bundles:");
	for (ConnectorInfo connectorInfo : connectorInfos) {
		ConnectorKey key = connectorInfo.getConnectorKey();
		System.out.println(key.toString());
	}

	// 4. Identifikátor určitého konektoru.
	ConnectorKey ffKey = new ConnectorKey("org.identityconnectors.ssh",
	"1.0.956",
	"org.identityconnectors.ssh.SSHConnector");
	 ConnectorInfo ffConInfo = manager.findConnectorInfo(ffKey);

	 // 5. Načítá defaultní konfiguraci konektoru.
	 APIConfiguration ffConfig = ffConInfo.createDefaultAPIConfiguration();

	 // 6. Vypsání názvů konfiguračních údajů.
	 ConfigurationProperties ffConfigProps = ffConfig.getConfigurationProperties();
	 for (String name : ffConfigProps.getPropertyNames()) {
		 System.out.println("property: " + name);
	 }	

	 // 7. Vypsání názvů konektorem podporovaných operací.
	 for (Class<? extends APIOperation> supportedOp : ffConfig.getSupportedOperations()) {
		 System.out.println("supported operation: " + supportedOp.getSimpleName());
	 }

	 // 8. Konfigurace konektoru.
	 ffConfigProps.setPropertyValue("host", "localhost");
	 ffConfigProps.setPropertyValue("port", 22);
	 ffConfigProps.setPropertyValue("username", "test");
	 ffConfigProps.setPropertyValue("password", new GuardedString("demo1234".toCharArray()));			 			 			 

	 ffConfigProps.setPropertyValue("user", "/home/jarda/workspace/bcv-idm/Realizace/ssh_connector/skripty_RT/sshuni_rt.sh");
	 ffConfigProps.setPropertyValue("createUser", "/home/jarda/workspace/bcv-idm/Realizace/ssh_connector/skripty_RT/sshuni_rt.sh");
	 ffConfigProps.setPropertyValue("deleteUser", "/home/jarda/workspace/bcv-idm/Realizace/ssh_connector/skripty_RT/sshuni_rt.sh");
	 ffConfigProps.setPropertyValue("enableUser", "/home/jarda/workspace/bcv-idm/Realizace/ssh_connector/skripty_RT/sshuni_rt.sh");
	 ffConfigProps.setPropertyValue("disableUser", "/home/jarda/workspace/bcv-idm/Realizace/ssh_connector/skripty_RT/sshuni_rt.sh");
	 ffConfigProps.setPropertyValue("updateUser", "/home/jarda/workspace/bcv-idm/Realizace/ssh_connector/skripty_RT/sshuni_rt.sh");			 			 

	 ffConfigProps.setPropertyValue("listObjects", "/home/jarda/workspace/bcv-idm/Realizace/ssh_connector/skripty_RT/sshuni_rt.sh");

	 ffConfigProps.setPropertyValue("escapeMode", "DOUBLED");
	 ffConfigProps.setPropertyValue("multiValueAttributes", new String[] {});
	 ffConfigProps.setPropertyValue("multiValueAttributesSeparator", ',');			 

	 // 9. Vytvoření instance konektoru, validace konfiguračních údajů, test spojení.
	 ConnectorFacade ffConnector = ConnectorFacadeFactory.getInstance().newInstance(ffConfig);
	 ffConnector.validate();
	 ffConnector.test();

	 //Nyní již můžeme nad konektorem volat implementované metody.

	 //Vytvoření nového uživatelského účtu "sokrates11".
	 Set<Attribute> attrs = new HashSet<Attribute>();
	 attrs.add(AttributeBuilder.build("Name", "sokrates1"));
	 attrs.add(AttributeBuilder.build("EmailAddress", "sokrates1@example.com"));
	 attrs.add(AttributeBuilder.build("Gecos", "sokr1"));
	 Uid uid = ffConnector.create(ObjectClass.ACCOUNT, attrs, null);
	 System.out.println("New users's UID: " + uid.getUidValue());

	 //Vyhledání uživatele "sokrates11".
	 ffConnector.getObject(ObjectClass.ACCOUNT, uid, null);

	 //Smazání uživatele "sokrates11".
         ffConnector.delete(ObjectClass.ACCOUNT, uid, null);			 

} catch (Exception ex) {
	ex.printStackTrace();
}

V prvním kroku nastavíme cestu k danému konektoru. Dále si vytvoříme instanci třídy ConnectorInfoManager, která udržuje seznam načtených konektorů. Ve třetím kroku si všechny načtené konektory vypíšeme. Poté si ve čtvrtém kroku vytvoříme referenci na konektor a v pátém načteme defaultní API konfiguraci. V šestém kroku vypíšeme názvy všech konfiguračních údajů a v sedmém všechny podporované operace na koncovém systému. V kroku číslo 8 nastavíme požadované konfigurační údaje a v devátém kroku již vytvoříme instanci konektoru. Nyní již můžeme volat požadované metody. Zkusíme si tedy vytvořit nový uživatelský účet na systému Request Tracker, pak ho vyhledáme a na závěr smažeme. Výstup celého běhu klientské aplikace je níže.

Available connector bundles:
ConnectorKey( bundleName=org.identityconnectors.ssh bundleVersion=1.0.956 connectorName=org.identityconnectors.ssh.SSHConnector )
property: host
property: port
property: username
property: password
property: privkey
property: privkeyPassword
property: hostkey
property: user
property: createUser
property: deleteUser
property: enableUser
property: disableUser
property: updateUser
property: group
property: createGroup
property: deleteGroup
property: updateGroup
property: listObjects
property: escapeMode
property: multiValueAttributes
property: multiValueAttributesSeparator
supported operation: GetApiOp
supported operation: SearchApiOp
supported operation: ScriptOnConnectorApiOp
supported operation: SchemaApiOp
supported operation: DeleteApiOp
supported operation: ValidateApiOp
supported operation: TestApiOp
supported operation: UpdateApiOp
supported operation: CreateApiOp
Thread Id: 1	Time: 2011-06-05 16:30:03.223	Class: org.identityconnectors.framework.api.operations.ValidateApiOp	Method: validate	Level: OK	Message: Enter: validate()
Thread Id: 1	Time: 2011-06-05 16:30:03.229	Class: org.identityconnectors.framework.api.operations.ValidateApiOp	Method: validate	Level: OK	Message: Return: null
Thread Id: 1	Time: 2011-06-05 16:30:03.240	Class: org.identityconnectors.framework.api.operations.TestApiOp	Method: test	Level: OK	Message: Enter: test()
Thread Id: 1	Time: 2011-06-05 16:30:03.267	Class: org.identityconnectors.ssh.SSHConnector	Method: test	Level: INFO	Message: SSHConnector - test
Thread Id: 1	Time: 2011-06-05 16:30:03.267	Class: org.identityconnectors.ssh.SSHConnection	Method: startConnection	Level: INFO	Message: Authentication via password.
Thread Id: 1	Time: 2011-06-05 16:30:03.574	Class: org.identityconnectors.ssh.SSHConnection	Method: startConnection	Level: INFO	Message: Succesfull connection.
Thread Id: 1	Time: 2011-06-05 16:30:03.575	Class: org.identityconnectors.ssh.SSHConnection	Method: dispose	Level: INFO	Message: Dispose connection.
Thread Id: 1	Time: 2011-06-05 16:30:03.587	Class: org.identityconnectors.framework.api.operations.TestApiOp	Method: test	Level: OK	Message: Return: null
Thread Id: 1	Time: 2011-06-05 16:30:03.605	Class: org.identityconnectors.framework.api.operations.CreateApiOp	Method: create	Level: OK	Message: Enter: create(ObjectClass: __ACCOUNT__, [Attribute: {Name=EmailAddress, Value=[sokrates1@example.com]}, Attribute: {Name=Gecos, Value=[sokr1]}, Attribute: {Name=Name, Value=[sokrates1]}], null)
Thread Id: 1	Time: 2011-06-05 16:30:03.618	Class: org.identityconnectors.ssh.SSHConnection	Method: startConnection	Level: INFO	Message: Authentication via password.
Thread Id: 1	Time: 2011-06-05 16:30:03.754	Class: org.identityconnectors.ssh.SSHConnection	Method: startConnection	Level: INFO	Message: Succesfull connection.
Thread Id: 1	Time: 2011-06-05 16:30:04.306	Class: org.identityconnectors.ssh.SSHConnection	Method: dispose	Level: INFO	Message: Dispose connection.
Thread Id: 1	Time: 2011-06-05 16:30:04.312	Class: org.identityconnectors.ssh.SSHConnection	Method: dispose	Level: INFO	Message: Dispose connection.
Thread Id: 1	Time: 2011-06-05 16:30:04.317	Class: org.identityconnectors.framework.api.operations.CreateApiOp	Method: create	Level: OK	Message: Return: Attribute: {Name=__UID__, Value=[user/143]}
New users's UID: user/143
Thread Id: 1	Time: 2011-06-05 16:30:04.322	Class: org.identityconnectors.framework.api.operations.GetApiOp	Method: getObject	Level: OK	Message: Enter: getObject(ObjectClass: __ACCOUNT__, Attribute: {Name=__UID__, Value=[user/143]}, null)
Thread Id: 1	Time: 2011-06-05 16:30:04.335	Class: org.identityconnectors.ssh.SSHConnection	Method: startConnection	Level: INFO	Message: Authentication via password.
Thread Id: 1	Time: 2011-06-05 16:30:04.439	Class: org.identityconnectors.ssh.SSHConnection	Method: startConnection	Level: INFO	Message: Succesfull connection.
Thread Id: 1	Time: 2011-06-05 16:30:04.838	Class: org.identityconnectors.ssh.SSHConnection	Method: dispose	Level: INFO	Message: Dispose connection.
Thread Id: 1	Time: 2011-06-05 16:30:04.858	Class: org.identityconnectors.ssh.SSHConnection	Method: dispose	Level: INFO	Message: Dispose connection.
Thread Id: 1	Time: 2011-06-05 16:30:04.859	Class: org.identityconnectors.framework.api.operations.GetApiOp	Method: getObject	Level: OK	Message: Return: {ObjectClass=ObjectClass: __ACCOUNT__, Attributes=[Attribute: {Name=Status, Value=[UNLOCK]}, Attribute: {Name=password, Value=[]}, Attribute: {Name=__UID__, Value=[user/143]}, Attribute: {Name=EmailAddress, Value=[sokrates1@example.com]}, Attribute: {Name=__NAME__, Value=[user/143]}, Attribute: {Name=Name, Value=[sokrates1]}], Name=Attribute: {Name=__NAME__, Value=[user/143]}, Uid=Attribute: {Name=__UID__, Value=[user/143]}}
Thread Id: 1	Time: 2011-06-05 16:30:04.862	Class: org.identityconnectors.framework.api.operations.DeleteApiOp	Method: delete	Level: OK	Message: Enter: delete(ObjectClass: __ACCOUNT__, Attribute: {Name=__UID__, Value=[user/143]}, null)
Thread Id: 1	Time: 2011-06-05 16:30:04.868	Class: org.identityconnectors.ssh.SSHConnection	Method: startConnection	Level: INFO	Message: Authentication via password.
Thread Id: 1	Time: 2011-06-05 16:30:05.040	Class: org.identityconnectors.ssh.SSHConnection	Method: startConnection	Level: INFO	Message: Succesfull connection.
Thread Id: 1	Time: 2011-06-05 16:30:05.132	Class: org.identityconnectors.ssh.SSHConnection	Method: dispose	Level: INFO	Message: Dispose connection.
Thread Id: 1	Time: 2011-06-05 16:30:05.134	Class: org.identityconnectors.ssh.SSHConnection	Method: dispose	Level: INFO	Message: Dispose connection.
Thread Id: 1	Time: 2011-06-05 16:30:05.134	Class: org.identityconnectors.framework.api.operations.DeleteApiOp	Method: delete	Level: OK	Message: Return: null

Závěrem

V tomto článku jsme si ukázali, co to jsou identity konektory, k čemu se používají v identity managementu. Zběžně jsme si popsali vývoj jednoho takového konektoru, který používáme v našem Identity Manageru CzechIdM. Jak již bylo uvedeno, tak oficiální dokumentace týkající se connector frameworku není mnoho. Případným zájemcům o bližší informace lze doporučit hlavně zdrojové kódy konektorů distribuovaných spolu s frameworkem a JavaDOC jednotlivých tříd. A snad i tento článek může sloužit jako takový letmý úvod do problematiky identity konektorů.

V případě dotazů mne neváhejte kontaktovat na mailu info@bcvsolutions.eu.

1 komentář u „Vývoj identity konektoru pro systém CzechIdM