 | レベル: 中級 安達 宜隆, IM&Lotus開発 / ソフトウェア開発研究所, IBM Japan
2009年 2月 20日 IBM OmniFind Enterprise Edition(以下OmniFind)は、製品として1ノード、2ノード、4ノード構成をサポートしており、検索システムの規模に応じて最適なノード構成を選択することが可能です。第1回目では、複数のOmniFindシステムに対して検索を行う、LocalFederatorを利用した検索アプリケーションのカスタマイズ手法を説明しました。しかし、LocalFederatorは検索結果の統合は行いますが、ユーザー情報の制御やグループ情報の取得など、セキュア検索に必要な処理は一切行いません。このような処理は、LocalFederatorで検索を行う前に、アプリケーション側で行う必要があります。第2回目の今回は、複数のOmniFindシステムを統合した際にもセキュア検索を可能とするために、前回のESSearchApplicationをさらにカスタマイズする方法を説明します。
セキュア検索とIdentity Management Component
OmniFindでは、検索を行ったユーザーが見る権限のないドキュメントを検索結果の中から除外する、セキュア検索をサポートしています。これによって、人事情報や営業情報など、いわゆるコンフィデンシャル情報を、権限がない人が検索結果として見えてしまうことを防げます。
セキュア検索を行うためには、検索時に、検索クエリとともに検索を行っているユーザーのACL(Access Control List)情報を渡す必要があります。製品に付属のESSearchApplicationでは、SiapiHelperクラスのcreateQueryメソッド内で、下記のコードのようにSIAPIのQueryオブジェクトにACL情報をセットしています。
リスト1 SiapiHelper.java
// if there is an XML security context, add it here
if (aclConstraints != null && aclConstraints.length() > 0) {
query.setACLConstraints("@SecurityContext::'" + aclConstraints + "'");
}
|
このACL情報は、Identity Management Component(以下IMC)と呼ばれるコンポーネントを利用することで、生成や取得・保存を行ったり、データソース毎のユーザー認証を行ったりすることができます。このIMCはIMC APIとして公開されており、製品に付属のESSearchApplicationは、このIMC APIを利用してACL情報を生成・取得し、セキュア検索を実現しています。
さて、前回説明したLocalFederatorを利用した検索アプリケーションですが、この検索アプリケーションでセキュア検索を行うためには、検索クエリを投げる前に、適切なACL情報をセットする必要があります。しかし、ここに1つ問題があります。複数のOmniFindシステムが存在する場合、各OmniFindシステムに別々のIMCが存在することになり、結果としてユーザーのACL情報も、OmniFindシステム毎に別々のものが生成されることになります。すなわち、デフォルトのESSearchApplicationが行っているように単一のIMCを利用して生成されたACLをセットすることはできず、複数のIMCから得られた複数のACL情報を統合し、1つのACL情報としたうえで検索クエリにセットする必要があります。
LocalFederatorと複数IMCを利用した検索アプリケーションの設計
前述した複数のIMCの統合を行うためには、検索アプリケーションのさらなるカスタマイズが必要になります。このような複数のOmniFindシステムに対して、LocalFederatorとIMCを利用してセキュア検索を行うためのアプリケーションの概要を図1に示します。
図1 LocalFederatorとIMCを統合した検索アプリケーションの概要
通常は、IMCは1つだけですので、ESSearchApplicationは1つのIMCとだけ処理を行うようになっています。しかし、今回のように、複数のOmniFindシステムを利用する場合は、ESSearchApplicationは複数のIMCと処理を行う必要があります。
デフォルトのESSearchApplicationでは、IMCの処理は全てIdentityManagementHelperクラスにまとめられています。IdentityManagementHelperインスタンスは各ユーザーのセッションに保存されており、検索時にIdentityManagementHelper内に保存されたACL情報が利用されることになります。複数のOmniFindシステムを利用する今回のアプリケーションでも同じように、IdentityManagementHelperクラスを利用することにします。ただし、OmniFindシステムが複数、すなわちIMC自体が複数ありますので、IdentityManagementHelperインスタンスも複数用意することになります。これらを配列として、各ユーザーのセッションに保存することにします。
各IdentityManagementHelperを利用してユーザーのACL情報を取得することはできますが、次にこれらのACL情報を統合する処理が必要になります。そこで、ACL情報を統合するために前回作成したFederatedSearchHelperクラスに、新しいstaticメソッドを追加することにします。このメソッドにIdentityManagementHelperの配列を渡すことで、複数のOmniFindシステムで利用可能なACL情報を生成するようにします。
こうして得られたACL情報を検索クエリにセットすることで、LocalFederatorを使った際にも、セキュア検索を実現できるようにします。
カスタマイズ
では実際にコードのカスタマイズを行っていきたいと思います。なお、コードに関しては前回のカスタマイズを前提にします。
最初に、IdentityManagementHelperインスタンスの配列から、統合されたACL情報を生成する処理を追加します。これは前回作成したFederatedSearchHelperクラスのstaticメソッドgetSecurityContextとして実装することにします。
リスト2 FederatedSearchHelper.java
public static String getSecurityContext(
IdentityManagementHelper[] imcHelpers, boolean encoded) {
SecurityContext federatedContext = new SecurityContext();
List identityList = new ArrayList();
List nativeTokenList = new ArrayList();
SecurityContext firstContext = imcHelpers[0].getSecurityContext();
federatedContext.setUserID(firstContext.getUserID());
federatedContext.setSSOToken(firstContext.getSSOToken());
for (int i = 0 ; i < imcHelpers.length ; i++) {
SecurityContext context = imcHelpers[i].getSecurityContext();
Identity[] identities = context.getIdentities();
String[] nativeTokens = context.getNativeTokens();
for (int j = 0 ; j < identities.length ; j++) {
identityList.add(identities[j]);
}
for (int j = 0 ; j < nativeTokens.length ; j++) {
nativeTokenList.add(nativeTokens[j]);
}
}
federatedContext.setIdentities(
(Identity[])identityList.toArray(new Identity[identityList.size()]));
federatedContext.setNativeTokens(
(String[])nativeTokenList.toArray(new String[nativeTokenList.size()]));
return federatedContext.serialize(encoded);
}
|
このgetSecurityContextの返値のStringがACL情報として、検索クエリにセットされることになります。
そして、IdentityManagementHelperを操作しているIdentityManagementActionクラスを変更します。
まず、IdentityManagementActionのexecuteメソッドで、IdentityManagementHelperを初期化している部分です。前回作成したFederatedSearchHelperクラスのgetPropertiesメソッドを利用して、複数のIdentityManagementHelperインスタンスを生成し、その配列をセッションに格納します。
リスト3 IdentityManagementAction.java(1)
IdentityManagementHelper[] imcHelpers = (IdentityManagementHelper[])
request.getSession().getAttribute("IdentityManagementHelpers");
if (imcHelpers == null) {
Properties[] props = FederatedSearchHelper.getProperties(properties);
imcHelpers = new IdentityManagementHelper[props.length];
for (int i = 0 ; i < props.length ; i++) {
imcHelpers[i] = new IdentityManagementHelper();
imcHelpers[i].initialize(userName, ssoCookie, props[i]);
}
request.getSession().setAttribute("IdentityManagementHelpers", imcHelpers);
}
|
これに続く処理も、配列に格納されたIdentityManagementHelperインスタンスを1つずつ処理するように書き換えます。基本的には、デフォルトのESSearchApplicationが単一のIdentityManagementHelperに行っていた処理を、IdentityManagementHelperの配列に格納された1つ1つのインスタンスに対して行うようにすれば大丈夫です。
ただし、最後のACLを生成する部分に関しては、先ほど作成したFederatedSearchHelperクラスのgetSecurityContextメソッドを利用して、統合されたACL情報を生成するようにします。ここで生成されたACL情報はユーザーのセッションに格納され、SiapiHelperで、検索クエリにセットされることになります。
リスト4 IdentityManagementAction.java(2)
//check if the default OmniFind Identity Management component has been enabled
//in the OmniFind Administration UI
//For federated IMC, detect all disabled case only
boolean ssoPropertiesEnabled = false;
boolean imcEnabled = false;
for (int i = 0 ; i < imcHelpers.length ; i++) {
Properties imcProperties = imcHelpers[i].getIdentityManagementProperties();
if (imcProperties == null || imcProperties.isEmpty()) {
if (logger.isLoggable(Level.INFO)) {
logger.info("execute - sso.properties file is empty. #" + i);
}
} else {
ssoPropertiesEnabled = true;
String isEnabled = imcProperties.getProperty(IMC_ENABLED);
if (isEnabled.equalsIgnoreCase("false")) {
if (logger.isLoggable(Level.INFO)) {
logger.info("execute - ldap.sso.enable is false");
}
} else {
imcEnabled = true;
if (imcHelpers[i].getSecurityContext().getIdentities().length == 0) {
//if the user's information stored in the database has not been loaded
//yet, then read it now
if (logger.isLoggable(Level.INFO)) {
logger.info("execute - reading from database");
}
imcHelpers[i].readSecurityContext();
}
}
}
}
if (!ssoPropertiesEnabled) {
if (command.equals("profileLink")) {
//if they are going to the My Profile page then let them know
//that the sso.properties file could not be retrieved
errors.add("info", new ActionError("errors.imc.properties.empty"));
}
} else if (!imcEnabled) {
if (command.equals("profileLink")) {
//if they are going to the My Profile page then let them know
//that the identity management component was disabled by the Administrator
errors.add("info", new ActionError("errors.imc.disabled"));
}
}
//process the request
boolean writeToDB = true;
if (command != null && !command.equals("")) {
if (command.equals("profile")) {
loadProfile(request, form, errors);
} else if (command.equals("profileLink")) {
loadProfile(request, form, errors);
writeToDB = false;
} else if (command.equals("validateProfile")) {
validateProfile(request, form, errors);
}
} else {
//no command specified, assume we should load the Profile information.
loadProfile(request, form, errors);
}
//write the information to the IMC database if we're supposed to
if (writeToDB) {
if (logger.isLoggable(Level.INFO)) {
logger.info("execute - writing to database");
}
for (int i = 0 ; i < imcHelpers.length ; i++) {
imcHelpers[i].writeSecurityContext();
}
}
//create the USC string and update the session
if (imcHelpers.length > 0) {
String securityContext =
FederatedSearchHelper.getSecurityContext(imcHelpers, true);
String decodedSecurityContext =
FederatedSearchHelper.getSecurityContext(imcHelpers, false);
if (PortletApiUtils.getUtilsInstance() != null) {
PortletRequest portletRequest = (PortletRequest) request;
PortletSession session = portletRequest.getPortletSession();
session.setAttribute("SecurityContext",
securityContext, PortletSession.APPLICATION_SCOPE);
session.setAttribute("DecodedSecurityContext", decodedSecurityContext,
PortletSession.APPLICATION_SCOPE);
}
request.getSession().setAttribute("SecurityContext", securityContext);
//this second string is used to display on the page if the
//Administrator has set the extraQueryData.show property to true
//the String value returned in this case will NOT be base64 encoded
request.getSession().setAttribute("DecodedSecurityContext",
decodedSecurityContext);
}
|
次にマイプロファイルで表示するデータソースのプロファイルを読み込むloadProfileメソッドを変更します。ここもIdentityManagementHelperの配列に対応して以下のように書き換えます。
リスト5 IdentityManagementAction.java(3)
protected void loadProfile(
HttpServletRequest request, ActionForm form, ActionErrors errors)
throws Exception {
if (logger.isLoggable(Level.FINE)) {
logger.fine("loadProfile - entered");
}
DynaActionForm theForm = (DynaActionForm) form;
IdentityManagementHelper[] imcHelpers = (IdentityManagementHelper[])
request.getSession().getAttribute("IdentityManagementHelpers");
// initialize the list of information to prompt for and verify
UserPrompt[] prompts =
(UserPrompt[]) request.getSession().getAttribute("userPrompts");
if (prompts == null) {
List promptList = new ArrayList();
for (int i = 0 ; i < imcHelpers.length ; i++) {
UserPrompt[] tempPrompts =
imcHelpers[i].getSecuredDataSources(request);
for (int j = 0 ; j < tempPrompts.length ; j++) {
promptList.add(tempPrompts[j]);
}
}
prompts =
(UserPrompt[])promptList.toArray(new UserPrompt[promptList.size()]);
request.getSession().setAttribute("userPrompts", prompts);
}
theForm.set("prompts", prompts);
boolean areSecuredSourcesPresent = false;
for (int i = 0 ; i < imcHelpers.length ; i++) {
if (imcHelpers[i].areSecuredSourcesPresent()) {
areSecuredSourcesPresent = true;
}
}
if (!areSecuredSourcesPresent) {
// there are NO secured sources to process
request.getSession().setAttribute("securedSourcesPresent", "false");
} else {
// process the user information to determine if it's
// already valid
request.getSession().setAttribute("securedSourcesPresent", "true");
processUserPrompts(request, form, errors, prompts, false);
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("loadProfile - returning");
}
}
|
processUserPromptsも同様に、配列に対応した形に書き換えます。
リスト6 IdentityManagementAction.java(4)
protected void processUserPrompts(
HttpServletRequest request, ActionForm form, ActionErrors errors,
UserPrompt[] prompts, boolean isApplied)
throws Exception {
if (logger.isLoggable(Level.FINE)) {
logger.fine("processUserPrompts - entered");
}
IdentityManagementHelper[] imcHelpers = (IdentityManagementHelper[])
request.getSession().getAttribute("IdentityManagementHelpers");
if (prompts != null && prompts.length > 0) {
for (int i = 0; i < prompts.length; ++i) {
for (int j = 0 ; j < imcHelpers.length ; j++) {
imcHelpers[j].updateIdentities(prompts[i], request.getLocale());
}
}
}
}
|
以上でカスタマイズは完了です。前回同様、config.propertiesに複数のOmniFindシステムのホスト名を設定することで、セキュア検索が可能になります。またマイプロファイルをクリックすると、セキュア検索可能なデータソースの一覧が出てくることが確認できると思います。
まとめ
複数のIMCと連携するようにカスタマイズした検索アプリケーションによって、複数のOmniFindシステムにまたがってセキュア検索を行うことが可能になりました。これによって、セキュア検索においても柔軟なノード構成を取ることが可能になり、より要件にあった検索システムを構築することが可能になります。
参考文献
著者について  | |  | 安達 宜隆は、ソフトウェア開発研究所のソフトウェア・エンジニアであり IBM OmniFind Enterprise Edition の開発・保守に携わっています。最近、寒い日が続き乾いていたところ、友人からVitaminを勧められたので早速試してみました。しかしすぐに飽きてしまい、途中でやめてしまいました。そんなものに頼らず、健康的な生活を送るべきだとつくづく実感しました。 |
記事の評価
|  |