2011年5月31日火曜日

MultitenancyとNamespaces API (Google App Engine)

 

Multitenancy

Multitenancyとは1つのアプリケーションインスタンスが複数のクライアント(ユーザーグループ)に対してサービスを行うソフトウェアアーキテクチャ。

ドキュメントによは、次のような用途が記載されている。

  • ユーザーインフォメーションを分類する
  • アドミニストレーターデータをアプリケーションデータから分離する
  • テストと完成品のために、分離されたデータストアインスタンスを作り出す
  • 多数のアプリケーションを一つのAppEngineのインスタンス上で走らせる

つまりうっかり混ざってほしくないデータを分けて保存したい場合に使えるようだ。Namespace APIを使ってネームスペース設定することでデータをネームスペースごとに分けて保存することができる。

ネームスペースに対応したAppEngine API

ネームスペースが使えるのは

  • Datastore
  • Memcache
  • Task Queue

だけ。

Blobstoreではネームスペースは使えない。

Namespace APIを使う

Multitenancyは Namespaces APIを使って実現する。使い方は簡単で、NamespaceManager.set(String) メソッドを呼ぶだけでカレントネームスペースを設定できる。

ネームスペースに設定できる文字列は100文字までの英数字、'-'、'_'、'.'で構成された文字列。アンダーバーで始まる文字列はシステム予約なので使えない。

ネームスペースを設定しなければカレントネームスペースはnullで、この場合各種AppEngine APIは空文字列("")のネームスペースを使用する。

ネームスペースはリクエストごとに設定する必要がある。各リクエストが開始された時点ではカレントネームスペースは設定されていない。

データ漏洩を避ける

ネームスペースで分離したデータがネームスペースを超えて漏洩しないように注意しなければならない。ネームスペースは文字列ひとつで設定できるのでうっかりすればネームスペースを超えたデータにアクセスできてしまう。

Blobsoreはネームスペースをサポートしないので、ネームスペースを使うアプリケーションでは、blobキーをネームスペースつきのDatastoreに保存してこのDatastoreデータを経由してblobにアクセスする。ブラウザから渡されたblobキーで直接blobにアクセスしてしまうと分離したいデータにアクセスできてしまうかもしれない。

Datastore

Datastore APIを呼び出す前にNamespaceManagerでカレントネームスペースを設定しておくだけで良い。

KeyやQueryを作るとき、APIはカレントネームスペースを参照してKey、Queryオブジェクトにネームスペースを設定する。先祖を指定してキーを作成すると、新しいキーは先祖キーのネームスペースを継承する。
なお、KeyやQueryに明示的にネームスペースを設定するJava APIは無い。

Key、Queryオブジェクトをシリアライズしたデータにはネームスペースも含まれる。これらをデシリアライズして使うときにはネームスペースが適切かどうか注意する必要がある。

カレントネームスペースが”a”のときに作られたキーは、シリアライズされてカレントネームスペース”b”でデシリアライズされてもキーのネームスペースは”a”のままで復元される。これをDatastoreにストアするとネームスペース”a”にストアされる。

信用できないソース(ブラウザなど)から渡されたKeyでDatastoreにアクセスするとネームスペースを超えてデータにアクセスしてしまう危険があるので、受け取ったキーのネームスペースが適切かどうか検証してから使用すること。

Memcache

MemcacheService を作成するときに明示的にネームスペースを設定しなければ、MemcacheService はメソッドが呼ばれるときにカレントネームスペースの設定を参照して memcache にアクセスする。

下のコード例はドキュメントから引用。

// Create a MemcacheService that uses the current namespace by
// calling NamespaceManager.get() for every access.
MemcacheService current =
  MemcacheServiceFactory.getMemcacheService();

// stores value in namespace "abc"
String oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
    current.put("key", value);  // stores value in namespace “abc”
} finally {
    NamespaceManager.set(oldNamespace);
}

MemcacheServiceFactory.getMemcacheService(String) で明示的にネームスペースを指定すると、カレントネームスペースを無視して指定したネームスペースで memcache にアクセスする。

MemcacheService boundMemcache =
  MemcacheServiceFactory.getMemcacheService("specific-namespace");
NamespaceManager.set("whatever-namespace");
// このレコードはネームスペース"specific-namespace"でストアされる。
boundMemcache.put("key3", "value3");

Task Queue

タスクが作られるときにNamespaceManagerにセットしたカレントネームスペースとGoogle Appsドメインも(もしあれば)共にキューにプッシュされる。

タスクが実行されるとき、カレントネームスペースとGoogle Appsドメインが復元される。もしタスクをaddするときにカレントネームスペースが設定されていなければ、タスクが実行されるとき空のネームスペースが設定される。

タスク名はネームスペースで分離されないのですべてのネームスペースに渡ってユニークな名前にしないとぶつかる。

Queueからタスクをプルする場合はネームスペースの機能は提供されない。自前でタスクのネームスペースを復元できるようにペイロードにネームスペースを入れるなどの策が必要。

API

Class NamespaceManager

カレントネームスペースを操作する機能を提供する。

カレントネームスペースはget()によって返される文字列。Datastore、Memcache、Task QueueのAPIで使われる。

ネームスペースに関連したクラス(例えば、Key、Query、MemcacheService)が作られるとき、ネームスペースが決まっていなければget()の呼び出しで決定される。もしget()がnullを返したらカレントネームスペースはセットされておらずこれらのAPIは空の("")ネームスペースを使う。

例:

NamespaceManager.set("a-namespace");
MemcacheService memcache =
  MemcacheServiceFactory.getMemcacheService();
// Store record in namespace "a-namespace"
memcache.put("key1", "value1");

NamespaceManager.set("other-namespace");
// Store record in namespace "other-namespace"
memcache.put("key2", "value2");

MemcacheService boundMemcache =
    MemcacheServiceFactory.getMemcacheService("specific-namespace");
NamespaceManager.set("whatever-namespace");
// このレコードはネームスペース"specific-namespace"でストアされる。
boundMemcache.put("key3", "value3");

MemcacheService memcache (上記の例で)はカレントネームスペースを使う。そして key1 がネームスペース「a-namespace」にストアされる。key2 はネームスペース「other-namespace」にストアされる。カレントネームスペースに優先してデータを特定のネームスペースにストアもできる。上記の例で key3 はネームスペース「specific-namespace」にストアされる。

Task Queueの Queue.add() メソッドは追加されるタスクの中にNamaspaceManagerの設定を転送し、追加されたタスクはタスクを作ったものと同じカレントネームスペースで実行される。

例外的に、カレントネームスペースが設定されていないとき(つまりget()がnullを返すとき)は空のネームスペース("")が作られたタスクのリクエストに転送される。

メソッド

public static void set(java.lang.String newNamespace)

    ネームスペースに関連したサービスのネームスペース初期化に使われる値をセットする。

    Parameters:
        newNamespace - 新しいネームスペース。
    Throws:
        java.lang.IllegalArgumentException - ネームスペース文字列が正しくないとき。

public static java.lang.String get()

    カレントネームスペース設定またはnull(設定されていないとき)返す。

    もしカレントネームスペースが設定されていなければ、呼び出し元は ネームスペースに関連したすべてのサービスで空のネームスペース(“”)を使うべきである。

public static java.lang.String getGoogleAppsNamespace()

    このリクエストのGoogle Appsドメインを返す。または、空の文字列を返す。

public static void validateNamespace(java.lang.String namespace)

    ネームスペース文字列の妥当性を検証する。

    Throws:
        java.lang.IllegalArgumentException - ネームスペース文字列のフォーマットが正しくないとき。

2011年5月28日土曜日

アプリケーションへのアクセスを制限する (Google App Engine)

URLパターンでアクセスを制限する

特定のURLパターンに対してアクセスを制限するには web.xml のsecurity-constraint要素で定義できる。

web-app要素(ルートの要素)の下にsecurity-constraint要素を追加する。

<security-constraint>

    <web-resource-collection>
        <web-resource-name>admin tool</web-resource-name>
        <url-pattern>/admin-tool*</url-pattern>
    </web-resource-collection>

    <user-data-constraint>
        <role-name>admin</role-name>
    </user-data-constraint>

</security-constraint>

アクセス可能なロールをadminとしておくと、AppEngineアプリの設定画面で管理者として設定したGoogleアカウントでしかアクセスできなくなる。Googleにログインせずに該当のURLにアクセスした場合は自動的にGoogleアカウントへのログイン画面へリダイレクトされる。

プログラムで動的にアクセスを制限する

開発環境とGAEのサーバー環境でアクセス制限方法を変えたり、非公開バージョンだけにアクセス制限をかけたい場合など、security constraint では柔軟性に欠ける場合にはアプリ自前のコードでアクセス制限をかけることができる。ただし、静的ファイルにはこの方法は使えない。

ここで言う”管理者”もsecurity-constraintの場合と同じ、アプリの設定画面で設定したGoogleアカウントのことを指す。

次のAPIはアクセス制限の役に立つ。

SystemProperty.applicationId
リクエストされたホスト名がアプリのホスト名かどうか判断するときに使う。

SystemProperty.environment
アプリが稼動する環境が開発環境かGAEサーバー環境かを判断するときに使う。

UserService#isUserAdmin()
Googleアカウントにログイン中のユーザーがアプリの管理者かどうかを判断するときに使う。

HttpServletRequest#getUserPrincipal()
ユーザーがログインしているかを判断するときに使う。

コード例

UserService userService = UserserviceFactory.getUserService();
if ( request.getUserPrincipal() != null
  && userService.isUserAdmin() ) {
    // アクセス許可
}
else {
    // アクセス拒否
}