2009/12/02

RhinoのJavaクラスのアクセス制限

公開サーバはスクリプトの書き込みも公開したいので放置してあったセキュリティ回りをやることにした。

サーバ側のJavaScriptはRhinoを使っているのだけれどもデフォルトだとJavaのクラスを全て使えてしまう。
java.io.* とか使われちゃうとセキュリティ的にいろいろまずいので制限したい。

Rhinoはそんな時の為に ClassShutter と言う機能を用意している。
使い方は凄く簡単で ClassShutter インターフェースを実装して実行Contextに設定するだけ。
メソッド visibleToScripts() はフルパスのクラス名を引数に取るので使って良いクラスならば true を返す。

http://www.mozilla.org/rhino/apidocs/org/mozilla/javascript/ClassShutter.html

public Interface ClassShutter {
boolean visibleToScripts(String fullClassName);
}

Context.getCurrentContext().setClassShutter(new ClassShutterImpl());



これを使ってアクセス制限をすることにする。
細かい指定ができる様に config のプロパティ permitClass を追加する。
- permitClass[*][0] : クラス名
-- ".*",".**"で終るクラス名はマスクとする。
- permitClass[*][1..n] : ロール名のリスト
-- "*" は全員OKを意味する。
-- "${deny}" は使用禁止を意味する。
- 上から順で最初にヒットした物が有効。
- いずれもヒットしなければ使用禁止。


config.permitClass = [
// Visitor
["java.lang.Object", "*"],
["java.lang.String", "*"],
["javax.servlet.**", "*"],
["kotemaru.wsjs.ssjs.SsjsEnv", "*"],

// Member
["java.beans.**", "member"],
["java.math.**", "member"],
["java.text.**", "member"],
["java.util.**", "member"],
["java.sql.DriverManager", "${deny}"],
["java.sql.**", "member"],
["kotemaru.wsjs.ssjs.XMLHttpRequest", "member"],
];


ここでアクセス制限の対象となるのはログインユーザではなくクラスを参照しているページの所有ユーザとなる。
WSJSではページの所有者の概念が無いので当該ページに write 権を持つユーザは全て所有ユーザとする。
つまり上の例では java.beans.* にアクセスするには write 権を持つユーザ全員が member ロールを持ってなければならない。

と、これだとユーザが増えると処理が∞になってしまう。
回避するにはロールに継承の概念が必要だがとりあえず TODO としておこう。

現状、運用で考えている write 権はこれだけなので問題無い。

config.permitWrite = [
["/home/${user}/", "${user}"],
];

write 権の全く無いページはシステムが用意した物として全てのクラスが使える。

DataSource でも同じ事しないといけないんだよね..




2009/11/28

RhinoからDBを使うラッパー

RhinoからDBを使う為のラッパーのAPIを決めた。

方針としては
- お手軽に使える事。DBのカラム==JSのプロパティとして扱える。
- 性能は考慮しない。性能が要求される場合はJDBCに逃げる。
- ややこしい SQL はそのまま渡せる。SQL を隠蔽することはしない。
とした。


テーブルの生成はこんな感じ。

var db = new DataBase(dataSource);
db.createTable("table_name", {
pkey: "integer NOT NULL GENERATED ALWAYS AS IDENTITY primary key",
title: "varchar(100)",
genre: "varchar(20)",
making_year: "varchar(20)",
country: "varchar(40)",
avg_point: "integer",
max_point: "integer",
min_point: "integer",
datetime: "timestamp",
description: "varchar(2000)"
});



行の挿入はこんな感じ。
- デフォルトが有る場合は省略可。
- 存在しないカラムは現状はエラー。チェックは重そうなので。

var db = new DataBase(dataSource);
db.insert("table_name", {
title: "ほげほげ",
genre: "hoge",
making_year: "2000",
country: "日本",
datetime: "2009-11-28 10:57:25",
description: "ほげほげーほげほげ"
});
db.close();


行の更新はこんな感じ
- UPDATE table_name SET description="ほげ" WHERE pkey=1

var db = new DataBase(dataSource);
var title_id = 1;
db.update("table_name", {description: "ほげ"}, {pkey: title_id});
db.close();



SELECTはこんな感じ。
- SELECT FROM table_name WHERE genre="hoge" ODER BY datetime ASC

var db = new DataBase(dataSource);
var sel = db.select("table_name", {genre: "hoge"}, {datetime: "ASC"});
var json = sel.getRows(1, 20);
db.close();
/*
json = [
{pkey:1, title:"ほげほげ", genre: "hoge", making_year:"2000", …},
:
];
*/


try-catch-finally で DB接続 -> commit/rollback をするラッパーを用意しておく。
こんな感じ。

EigaDB.addTitle = function(params) {
return DataBase.transaction(DATA_SOURCE, function(db) {
db.insert(TITLES, params);
});
}



その他、細かい関数も追加して実装した。
DBのレコードがそのまんま JavaScript のオブジェクトとして扱えるのはかなり楽だ。
RPJSを経由する事で DBとクライアントがほぼ直通になる。

実装は /lib/DataBase.js になるがリリースは次の版で。



2009/11/24

WSJSの使い方、その6(組み合せ技)

WSJSの各々の機能を組み合わせて使ってみます。
データベース上のシステムテーブルの内容を全部表示するサンプルです。
SELECT を選択すると非同期でデータベースにアクセスし表示用<table>を更新します。

sysTable.html:

getSysTable.exjs: 非同期処理の関数です。

getSysTable.rpjs: データベースの内容を JSON に変換して返すサーバ側関数です。

http://localhost:8080/wsjs/mix/sysTable.html にアクセスします。

データベースの内容が表示されています。

sysTable.html について説明します。

  • .js の読み込みは割愛します。
  • tableData の初期値として SYSTABLES の内容を得ています。
  • <select>タグをこの内容で初期化しています。
  • <select>タグの onchange でテーブルの更新スレッドを起動しています。
getSysTable.exjs について説明します。
  • yield を使って非同期でデータベースにアクセスします。
  • データベースの応答を受け取った後、<table> を更新しています。
getSysTable.rpjs について説明します。
  • __ENV__ は実行環境を表すシステム変数で実行環境にアクセスする為の機能を提供します。
  • getDBConnection() はデータベースへの接続を得る関数で戻り値は java.sql.Connection です。
  • データベースへのアクセスは JDBC をそのまま使います。
  • ResultSet の内容を json 変数にそのままコピーして戻り値としています。
このサンプルは 「EXJS から RPJS を呼び出しその結果を JSCP で表示する。」 と言うWSJSを使う上で基本的なパターンとなります。

2009/11/23

WSJSの使い方、その5(疑似スレッド:EXJS)

JavaScriptはスレッドを持っていない為、 処理待ちを行うには複雑な非同期処理を記述しなければなりません。 これはアプリケーションの実装において大きな負担となっていました。

WSJSは非同期処理を同期処理の様に記述できる機能を提供しています。 この機能を使うと疑似的にスレッドが使えます。

疑似スレッドを使用するには幾つか手順があります。

  • 拡張子を .exjs としたファイルで関数を定義すること。
  • 関数の中で yield 演算子を使用すること。
  • 関数の戻り値に対し next() 関数を呼び出すこと。
実際に動かしてみます。
以下のサンプルコードを d:/test/exjs/ に置きます。

steps.html:

steps.exjs: 疑似スレッドとなる関数

steps.jscp: サーバ側で時間のかかる関数

http://localhost:8080/wsjs/exjs/steps.html にアクセスします。
「GO-1」をクリックしてスレッドを開始します。 「GO-3」,「GO-2」をクリックして各々のスレッドを開始します。 各々のスレッドは独立して進行していきます。 最終的に全てのスレッドが完了します。

「GO-1」のスレッドが開始した後もボタンの操作は可能ですし、 他のスレッドも開始できています。

steps.html について説明します。
  • steps.exjs で疑似スレッドとなる関数を読み込んでいます。
  • steps.rpjs で時間のかかるサーバ側関数を読み込んでいます。
  • onclick="startSteps(~).next()" で疑似スレットを起動してます。
steps.exjs について説明します。
  • 関数 startSteps() が疑似スレッド関数となります。
  • yield step1Async(__This) はサーバ側関数の step1() を非同期に呼び出しています。
  • 非同期呼び出しを行う場合は関数名の後に "Async" を付けて最初の引数に __This を渡します。
  • 引数で渡されたノードに進行状況を書き込みます。
steps.jscp について説明します。
  • 時間のかかる処理のダミーとして3秒間スリープしています。

yield の仕様については以下を参照してください。 https://developer.mozilla.org/ja/New_in_JavaScript_1.7
尚、__This はWSJSの独自拡張でジェネレータの返したイテレータを示しています。


2009/11/22

WSJSの使い方、その4(動的HTML:JSCP)

WSJSはPHPやJSP風のHTMLを動的生成するJSCPと言う機能を持っています。 但し、サーバ側ではなくクライアント側で動作します。

以下のサンプルHTMLを d:/test/jscp/select.html に置きます。

http://localhost:8080/wsjs/jscp/select.html にアクセスします。 <select>タグの内容がスクリプトによって展開されています。

「野菜」をクリックします。 <select>タグの内容がスクリプトによって更新されています。

select.htmlについて説明します。

  • JSCPはXHTMLを使用するためDOCTYPEとHTMLの宣言は必須です。
  • "http://jscp/" の名前空間定義も必須です。プレフィックスは自由です。
  • JSCP.js はJSCPの本体です。
  • <jscp:jscp>タグの内側では <%~%> に JavaScript が記述できます。
  • <%= 式%> は JavaScript の式の値に置き換わります。
  • コメントで括られているのは <% %> がXHTMLエラーとなる為です。
JSCPはHTMLでしかありませんのでサーバに負担をかけません(というかWSJSサーバが無くても動作します)。 又、サンプルの様にクライアント側で再度、動的な展開が行えるので リッチクライアントを非常に簡単に実装できます。

2009/11/20

WSJSの使い方、その1(Webサーバ)

WSJSの最も基本的な使い方は単純なWebサーバとしての使い方です。

環境設定の documentRoot に指定したディレクトリの配下がそのままServlet のコンテンツとして公開されます。

まず、WSJSを導入します。
/old/img/auone/wsjs/docs/install.html

http://localhost:8080/wsjs/admin/ にアクセスしてルートディレクトリを指定します。


指定したディレクトリにHTMLファイルを置きます。


test.html:

<html>
<body>
<div>Hello world</div>
</body>
</html>



http://localhost:8080/wsjs/test.html にアクセスすると test.html が表示されます。
又、編集結果はすぐに反映されます。



Subject: WSJSの使い方、その2(テストサーバ:SSJS)
Content-type: lovelog/text
Tags: WSJS
Date: 2009/11/20
Public: yes

WSJSの便利な使い方はテストサーバとしての使い方です。

WSJSは Servlet を JavaScript で記述する機能を持っています。
特殊なヘッダを返す様なテストページをコンパイルもデプロイもすること無くファイルを1つ置くだけで作る事ができます。

ディレクトリに拡張子を .ssjs としたファイルを置きます。



d:/test/docroot/403-test.ssjs:

function doGet(req, res) {
res.setStatus(403);
res.setHeader("X-Custom-header", "custom-data");
var message = "Accsess denied.";
var html = <html><body><div>{message}</div></body></html>;
res.getWriter().write( html );
}



そして、http://localhost:8080/wsjs/403-test.ssjs にアクセスし FireBug でヘッダを確認してみます。
指定した通りの結果が返されています。



403-test.ssjs について解説します。
関数 doGet() は Servlet の doGet() と全く同じ意味です。
引数の req,res は HttpServletRequest,HttpServletResponse のインスタンスです。
WSJSは JavaScript に Rhino を使っていますので Java のインスタンスはほぼそのまま使用できます。

2,3行目は HttpServletResponse のメソッドを使って終了コードとカスタムのヘッダを設定しています。
4,5行目は Rhino の E4X 記法を使って応答の本文を生成しています。{message} は変数 message の内容に置き換わります。
6行目で本文を書き出して終了です。

POST メソッドを受けたければ同様に doPost() 関数を定義します。
おそらく Servlet の実装をした事の有る人であれば何も考えずに使えるでしょう。

RhinoでJavaScriptからjavaを使う方法は以下のページが詳しいです。
https://developer.mozilla.org/ja/Core_JavaScript_1.5_Guide/LiveConnect_Overview

E4Xの使い方は以下のページが詳しいです。
https://developer.mozilla.org/ja/E4X_Tutorial


Subject: WSJSの使い方、その3(リモート呼出:RPJS)
Content-type: html/text
Tags: WSJS
Date: 2009/11/20
Public: yes



SSJSを使うことで御手軽にServletが作れる様になりました。

しかし、クライアントから特にJavaScriptから呼び出そうとすると
パラメータを作りURLを合成してXMLHttpRequestを作って呼び出さなければなりません。


JavaScriptからJavaScriptを呼び出すのに何でこんな面倒なことしなきゃいけないの?

と思うのが人情です。


WSJSでは拡張子を .rpjs とするだけでクライアントからサーバ内のJavaScriptを呼び出せます。
コンパイルもデプロイも必要有りません。



ディレクトリに拡張子を .rpjs としたファイルとそれを<script>タグで読み込むHTMLファイルを置きます。






d:/test/docroot/rpjs/call-rpjs.html:


<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"></meta>
<script src="../lib/json2.js"></script>
<script src="hello.rpjs"></script>
</head>
<body>
<input type="button" value="Hello"
onclick="alert( hello(navigator.userAgent) );" />
</body>
</html>





d:/test/docroot/rpjs/hello.rpjs:


function hello(name) {
return "Hello "+name
+". \n\nI am " +java.lang.System.getProperty("os.name");
}




http://localhost:8080/wsjs/rpjs/call-rpjs.html にアクセスします。




「Hello」ボタンをクリックします。


分かり難いですがサーバ側の os.name を応答しています。


call-rpjs.html から解説します。


  • 最初に読み込んでいる json2.js は通信用のライブラリでRPJSに必要です。
    私の著作物で無いので意図的に分離しています。
  • 次に読み込んでいる hello.rpjs がサーバ側関数です。実際には通信を行うスタブが返されます。
  • onclick の中で呼んでいる hello() はサーバ側で実行され戻り値はクライアントに返されます。


hello.rpjs を解説します。

  • 単純にJavaScript関数を定義しているだけです。
  • java.lang.System は Rhino の Java 呼び出し機能でサーバ側で動作している事を確認する為使っています。



通信にはJSON-RPCと言うプロトコルを使用しています。

興味のある方はこちらからどうぞ。http://json-rpc.org/





2009/11/18

WSJS-0.1.2 公開

WSJS-0.1.2 を公開します。
認証関係の調査結果が反映されています。
OpenID,CAS,LDAP,Servlet,localfile(独自)をサポートします。


WSJSは自身で管理するユーザDBを持ちません。
個々のWebアプリがユーザDBを持つのはシステム的には無駄でしかありません。
ユーザの囲い込みと言う意味合いが有ったのかもしれませんがその考え方は明らかに時代遅れでしょう。

と言うわけでWSJSはOpenID \(^o^)/ で行きます。
イントラだとCASの方が良いかも知れないけど。
いずれにしろ認証系終り! (結構ヘビーだった..)

ダウンロードはホームページからどうぞ。
/old/img/auone/wsjs/




2009/11/17

OpenID,CASユーザへのロールの付与

OpenIDの調査の結果からロール管理は認証とは完全に分離する事にした。
その上で安全にロールを付与使用とすると

- ロール管理は複数を並列かつ階層的に扱えること。
-- 基幹系と部門系で管理を分けるようなケースを想定。
-- 部門系のadmin権限が基幹系で通用しないような階層化が必要。

- 認証サーバによって適用するロールを細かく制御できること。
-- OpenIDによって認証されたユーザが信用できるとは限らないと言う状態が出て来た。
-- 自身の制御下にあるような信用できる認証サーバに認証されたユーザとその他のユーザを分離できないといけない。

ぐらいの条件が出て来る。
解決策として

- ロール管理をJavaScriptでConfigに書ける。
- ユーザは自身を認証したサーバを持つ。

を導入した。

設定ファイルはこんな感じ。
- 認証はOpenIDを使う。
- ロール管理はLDAPを使う。
- 但し、MyDomain.com 以外に認証されたユーザが強制的に guest 扱い。


config.pam = config.newJavaInstance({
"class": "kotemaru.auth.openid.PAMImpl",
"loginUrl" : "http://localhost:8080/wsjs/login/login-OpenID.ssjs",
"verifyUrl": "http://localhost:8080/wsjs/login/login-OpenID-verify.ssjs"
});

var ldapRoleManager = config.newJavaInstance({
"class": "kotemaru.auth.ldap.RoleManagerImpl",
"java.naming.provider.url": "ldap://localhost/",
"role.contextName": "ou=Group,dc=example,dc=com",
"role.memberAttr": "memberUid",
"role.nameAttr": "cn",
});

config.roleManager = new Packages.kotemaru.auth.JSRoleManager({
getRoles: function(user) {
var IdP = user.idProvider.uniqName;
if (IdP != "http://MyDomain.com/auth") {
return ["users", "guest"];
}
return ldapRoleManager.getRoles(user);
},
});


全部LADPの場合はこんな感じ。

config.pam = config.newJavaInstance({
"class": "kotemaru.auth.ldap.PAMImpl",
"loginUrl" : "/wsjs/login/login.html",
"java.naming.provider.url": "ldap://localhost/",
"java.naming.security.authentication": "simple",
"java.naming.security.principal": "cn=${user},ou=People,dc=example,dc=com",
"java.naming.security.credentials": "${passwd}",
});
config.roleManager = config.newJavaInstance({
"class": "kotemaru.auth.ldap.RoleManagerImpl",
"java.naming.provider.url": "ldap://localhost/",
"role.contextName": "ou=Group,dc=example,dc=com",
"role.memberAttr": "memberUid",
"role.nameAttr": "cn",
});


これでスッキリかつ柔軟に認証系が記述できる。



プロフィール
20年勤めた会社がリーマンショックで消滅、紆余曲折を経て現在はフリーランスのSE。 失業をきっかけにこのブログを始める。

サイト内検索

登録
RSS/2.0

カテゴリ

最近の投稿【WSJS】

リンク

アーカイブ