認証周りを整理
自分で訳しといてあれですが、http://kuenishi.appspot.com/couchdb_definitive_guide_l10n_jp/21_security.html で言及されていないことがあります。それはユーザーデータベースの存在。動作までは確認していませんが、ソースコード(Erlang/JSのテスト)を確認したのでまとめます。
認証モジュールは入れ替え可能。
CouchDBの認証モジュールはErlangで記述さえすれば入れ替え可能です。etc/couchdb/default.ini に次のように設定します。
authentication_handlers = {couch_httpd_oauth, oauth_authentication_handler}, {couch_httpd_auth, default_authentication_handler}
これは couch_httpd_oauth というモジュールの oauth_authentication_hander という関数と、couch_htpd_auth の .... を使います、という設定です。
で、何が設定できるの?
grep -R authentication_hander * | grep export しました。
couch_httpd_auth.erl:-export([default_authentication_handler/1,special_test_authentication_handler/1]). couch_httpd_auth.erl:-export([cookie_authentication_handler/1]). couch_httpd_auth.erl:-export([null_authentication_handler/1]). couch_httpd_oauth.erl:-export([oauth_authentication_handler/1, handle_oauth_req/1, consumer_lookup/2]).
5つ見つかっています。それぞれソースを読みます。oauthは今回はパス!
{couch_httpd_auth, default_authentication_handler}
% couch_httpd_auth 63行目付近 default_authentication_handler(Req) -> case basic_username_pw(Req) of {User, Pass} -> case couch_server:is_admin(User, Pass) of true -> Req#httpd{user_ctx=#user_ctx{name=?l2b(User), roles=[<<"_admin">>]}}; false -> throw({unauthorized, <<"Name or password is incorrect.">>}) end;
ベーシック認証でユーザー名とパスワードが渡ってきたら、couch_server:is_admin で調べるようです。成功すれば、現在のリクエストにuser_ctxにユーザー名と _adminというロールをつけて返すので、その後の処理(show/list 等)でユーザー名とロール"_admin"が使えるわけ。失敗したらunauthorized。
nil -> case couch_server:has_admins() of true -> Req; false -> case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of "true" -> Req; % If no admins, and no user required, then everyone is admin! % Yay, admin party! _ -> Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}} end end end.
Basic認証がこない(Authorizationヘッダーがこない)場合は、has_adminsでadminの設定がされているかどうかを調べて、設定されていれば、リクエストには何もしない(認証されていない)でそのまま返し、設定されていない場合で require_valid_user が true になっていればやはりリクエストには何もしない(認証されていない)でそのまま返す。require_valid_userがfalseの場合は、認証はしないけれど、_adminユーザーであると見なしてリクエストを返す、ということになります。
デフォルトでは、皆すべて管理ユーザーです。
確かに、このことを意味していますね。
さらに、認証ソースを確かめるために、couch_server:is_admin/2 だとかその辺をみます。
% couch_server.erl 103行目付近 is_admin(User, ClearPwd) -> case couch_config:get("admins", User) of "-hashed-" ++ HashedPwdAndSalt -> [HashedPwd, Salt] = string:tokens(HashedPwdAndSalt, ","), couch_util:to_hex(crypto:sha(ClearPwd ++ Salt)) == HashedPwd; _Else -> false end.
ということで、認証ソースは、configファイルで確定ですね。
まとめ:default_authentication_handlerはBasic認証でlocal.ini にユーザー名とパスワードの組を書いておく。
{couch_httpd_auth, special_test_authentication_handler}
名前からしてテスト用だろ、って話はありますが一応確認。
special_test_authentication_handler(Req) -> case header_value(Req, "WWW-Authenticate") of "X-Couch-Test-Auth " ++ NamePass -> % NamePass is a colon separated string: "joe schmoe:a password". [Name, Pass] = re:split(NamePass, ":", [{return, list}]), case {Name, Pass} of {"Jan Lehnardt", "apple"} -> ok; {"Christopher Lenz", "dog food"} -> ok; {"Noah Slater", "biggiesmalls endian"} -> ok; {"Chris Anderson", "mp3"} -> ok; {"Damien Katz", "pecan pie"} -> ok; {_, _} -> throw({unauthorized, <<"Name or password is incorrect.">>}) end, Req#httpd{user_ctx=#user_ctx{name=?l2b(Name)}};
はい、この認証モジュールを有効にした場合は、コミッターの名前とパスワードをWWW-Authenticateヘッダーに?*1 X-Couch-Test-Auth に続く文字列でいれておきます。
まとめ: {couch_httpd_auth, special_test_authentication_handler} はテスト用
{couch_httpd_auth, cookie_authentication_handler}
本命です。これの情報が実はあまりない。ソースを読むと、、
% 89行目 cookie_authentication_handler(Req) -> DbName = couch_config:get("couch_httpd_auth", "authentication_db"), case cookie_auth_user(Req, ?l2b(DbName)) of % Fall back to default authentication handler nil -> default_authentication_handler(Req); Req2 -> Req2 end.
設定ファイルに記述した authentication_db のデータベースを使って認証します。ということらしい。認証に失敗したら、default_authentication_handlerに切り替える模様。じゃあ、うまくいく場合は...?
% 209行目 cookie_auth_user(#httpd{mochi_req=MochiReq}=Req, DbName) -> case MochiReq:get_cookie_value("AuthSession") of undefined -> nil; [] -> nil; Cookie -> case couch_db:open(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of {ok, Db} -> try AuthSession = couch_util:decodeBase64Url(Cookie),
AuthSessionというクッキーを取り出して、あればDBを開きます。ちなみに、このクッキーを生成するには /_session APIを使います。
[User, TimeStr | HashParts] = string:tokens(?b2l(AuthSession), ":"), % Verify expiry and hash {NowMS, NowS, _} = erlang:now(), CurrentTime = NowMS * 1000000 + NowS,
セッションからユーザー、時刻、ハッシュ部分を取り出していますね。あと現在時刻の計算。
case couch_config:get("couch_httpd_auth", "secret", nil) of nil -> nil; SecretStr ->
設定ファイルに secret を設定しないと問答無用でnilが返され認証に失敗(default_authenticaiton_handlerへフォールバック)するようです。
Secret = ?l2b(SecretStr), case get_user(Db, ?l2b(User)) of nil -> nil; Result ->
シークレットを取り出しつつ、データベースからユーザー名をキーにしてドキュメント取り出す。取り出した結果はResultに束縛されているドキュメントになりますね。
UserSalt = proplists:get_value(<<"salt">>, Result, <<"">>), FullSecret = <<Secret/binary, UserSalt/binary>>, ExpectedHash = crypto:sha_mac(FullSecret, User ++ ":" ++ TimeStr), Hash = ?l2b(string:join(HashParts, ":")), Timeout = to_int(couch_config:get("couch_httpd_auth", "timeout", 600)), ?LOG_DEBUG("timeout ~p", [Timeout]),
Resultからユーザーのsaltを取り出して、完全なSecretを作り出す。で期待するハッシュを計算する一方で、クッキーからもハッシュを取り出す。ついでに、タイムアウト値も計算する。
case (catch erlang:list_to_integer(TimeStr, 16)) of TimeStamp when CurrentTime < TimeStamp + Timeout
でタイムアウトしていなくて、
andalso ExpectedHash == Hash -> TimeLeft = TimeStamp + Timeout - CurrentTime, ?LOG_DEBUG("Successful cookie auth as: ~p", [User]), Req#httpd{user_ctx=#user_ctx{ name=?l2b(User), roles=proplists:get_value(<<"roles">>, Result, []) }, auth={FullSecret, TimeLeft < Timeout*0.9}}; _Else -> nil
期待するハッシュとクッキーから得られたハッシュが一致するならば、リクエストオブジェクトにユーザー名と、ユーザーDBに定義されたロールをくっつけて返す!
まとめ: {couch_httpd_auth, cookie_authentication_handler} 設定ファイルに記述したauthentication_dbを認証用のデータベースとして使うことができます!
つまり、CouchDB自体としては _admin というロールしか定義していないけれど、cookie_authentication と authentication_db を使えばアプリケーションでロールを自由に定義できる、ってことですね。ちなみに、authentication_db のデフォルトの設定は users です。このソースを見る限りは、このデータベースに対して次のようにユーザーをドキュメントとして登録しておけばOK、だと思います。まだ確認してません、明日やります。
{ _id: "ここは任意", salt: "ここにsaltを", password_sha: SHA1("パスワード" + "salt値")のhexフォーマット, username: ユーザー名, type: "user", roles: ["role1", "role2", ...] // _admin もOKだと思う }
{couch_httpd_auth, null_authentication_handler}
null_authentication_handler(Req) -> Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}.
全員 _admin 君です。
補足
今回大発見のCookie Authは per instance です。CouchDBインスタンス全体で共通のユーザーデータベースを使うことになります。ということで、CouchDB上でユーザードメインの異なる複数のサービスを運用するには向きません。ちなみにこんなコメントアウトがありました(笑。
% Cookie auth handler using per-db user db % cookie_authentication_handler(#httpd{path_parts=Path}=Req) -> % case Path of % [DbName|_] -> % case cookie_auth_user(Req, DbName) of % nil -> default_authentication_handler(Req); % Req2 -> Req2 % end; % _Else -> % % Fall back to default authentication handler % default_authentication_handler(Req) % end.
まぁ気持ちはわかります。それにしてもErlangは読みやすくていいですね。すらすら書けるようになりたい。
*1:これ、レスポンスヘッダー用のヘッダ名じゃないの?