Generated on 2009-08-09 21:22:56 with etap 0.3.4.
| Name | Total lines | Lines of code | Total coverage | Code coverage | ||||
| couch_httpd_auth | ?? | ?? | ?? |
|
....1 % Licensed under the Apache License, Version 2.0 (the "License"); you may not ....2 % use this file except in compliance with the License. You may obtain a copy of ....3 % the License at ....4 % ....5 % http://www.apache.org/licenses/LICENSE-2.0 ....6 % ....7 % Unless required by applicable law or agreed to in writing, software ....8 % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT ....9 % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the ...10 % License for the specific language governing permissions and limitations under ...11 % the License. ...12 ...13 -module(couch_httpd_auth). ...14 -include("couch_db.hrl"). ...15 ...16 -export([default_authentication_handler/1,special_test_authentication_handler/1]). ...17 -export([cookie_authentication_handler/1]). ...18 -export([null_authentication_handler/1]). ...19 -export([cookie_auth_header/2]). ...20 -export([handle_session_req/1]). ...21 -export([handle_user_req/1]). ...22 -export([ensure_users_db_exists/1, get_user/2]). ...23 ...24 -import(couch_httpd, [header_value/2, send_json/2,send_json/4, send_method_not_allowed/2]). ...25 ...26 special_test_authentication_handler(Req) -> ...27 case header_value(Req, "WWW-Authenticate") of ...28 "X-Couch-Test-Auth " ++ NamePass -> ...29 % NamePass is a colon separated string: "joe schmoe:a password". ...30 [Name, Pass] = re:split(NamePass, ":", [{return, list}]), ...31 case {Name, Pass} of ...32 {"Jan Lehnardt", "apple"} -> ok; ...33 {"Christopher Lenz", "dog food"} -> ok; ...34 {"Noah Slater", "biggiesmalls endian"} -> ok; ...35 {"Chris Anderson", "mp3"} -> ok; ...36 {"Damien Katz", "pecan pie"} -> ok; ...37 {_, _} -> ...38 throw({unauthorized, <<"Name or password is incorrect.">>}) ...39 end, ...40 Req#httpd{user_ctx=#user_ctx{name=?l2b(Name)}}; ...41 _ -> ...42 % No X-Couch-Test-Auth credentials sent, give admin access so the ...43 % previous authentication can be restored after the test ...44 Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}} ...45 end. ...46 ...47 basic_username_pw(Req) -> ...48 case header_value(Req, "Authorization") of ...49 "Basic " ++ Base64Value -> ...50 case string:tokens(?b2l(couch_util:decodeBase64(Base64Value)),":") of ...51 [User, Pass] -> ...52 {User, Pass}; ...53 [User] -> ...54 {User, ""}; ...55 _ -> ...56 nil ...57 end; ...58 _ -> ...59 nil ...60 end. ...61 ...62 default_authentication_handler(Req) -> ...63 case basic_username_pw(Req) of ...64 {User, Pass} -> ...65 case couch_server:is_admin(User, Pass) of ...66 true -> ...67 Req#httpd{user_ctx=#user_ctx{name=?l2b(User), roles=[<<"_admin">>]}}; ...68 false -> ...69 throw({unauthorized, <<"Name or password is incorrect.">>}) ...70 end; ...71 nil -> ...72 case couch_server:has_admins() of ...73 true -> ...74 Req; ...75 false -> ...76 case couch_config:get("couch_httpd_auth", "require_valid_user", "false") of ...77 "true" -> Req; ...78 % If no admins, and no user required, then everyone is admin! ...79 % Yay, admin party! ...80 _ -> Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}} ...81 end ...82 end ...83 end. ...84 ...85 null_authentication_handler(Req) -> ...86 Req#httpd{user_ctx=#user_ctx{roles=[<<"_admin">>]}}. ...87 ...88 % Cookie auth handler using per-node user db ...89 cookie_authentication_handler(Req) -> ...90 DbName = couch_config:get("couch_httpd_auth", "authentication_db"), ...91 case cookie_auth_user(Req, ?l2b(DbName)) of ...92 % Fall back to default authentication handler ...93 nil -> default_authentication_handler(Req); ...94 Req2 -> Req2 ...95 end. ...96 ...97 % Cookie auth handler using per-db user db ...98 % cookie_authentication_handler(#httpd{path_parts=Path}=Req) -> ...99 % case Path of ..100 % [DbName|_] -> ..101 % case cookie_auth_user(Req, DbName) of ..102 % nil -> default_authentication_handler(Req); ..103 % Req2 -> Req2 ..104 % end; ..105 % _Else -> ..106 % % Fall back to default authentication handler ..107 % default_authentication_handler(Req) ..108 % end. ..109 ..110 % maybe we can use hovercraft to simplify running this view query ..111 get_user(Db, UserName) -> ..112 DesignId = <<"_design/_auth">>, ..113 ViewName = <<"users">>, ..114 % if the design doc or the view doesn't exist, then make it ..115 ensure_users_view_exists(Db, DesignId, ViewName), ..116 ..117 case (catch couch_view:get_map_view(Db, DesignId, ViewName, nil)) of ..118 {ok, View, _Group} -> ..119 FoldlFun = fun ..120 ({{Key, _DocId}, Value}, _, nil) when Key == UserName -> {ok, Value}; ..121 (_, _, Acc) -> {stop, Acc} ..122 end, ..123 case couch_view:fold(View, {UserName, nil}, fwd, FoldlFun, nil) of ..124 {ok, {Result}} -> Result; ..125 _Else -> nil ..126 end; ..127 {not_found, _Reason} -> ..128 nil ..129 % case (catch couch_view:get_reduce_view(Db, DesignId, ViewName, nil)) of ..130 % {ok, _ReduceView, _Group} -> ..131 % not_implemented; ..132 % {not_found, _Reason} -> ..133 % nil ..134 % end ..135 end. ..136 ..137 ensure_users_db_exists(DbName) -> ..138 case couch_db:open(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of ..139 {ok, Db} -> ..140 couch_db:close(Db), ..141 ok; ..142 _Error -> ..143 ?LOG_ERROR("Create the db ~p", [DbName]), ..144 {ok, Db} = couch_db:create(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]), ..145 ?LOG_ERROR("Created the db ~p", [DbName]), ..146 couch_db:close(Db), ..147 ok ..148 end. ..149 ..150 ensure_users_view_exists(Db, DDocId, VName) -> ..151 try couch_httpd_db:couch_doc_open(Db, DDocId, nil, []) of ..152 _Foo -> ok ..153 catch ..154 _:Error -> ..155 ?LOG_ERROR("create the design document ~p : ~p", [DDocId, Error]), ..156 % create the design document ..157 {ok, AuthDesign} = auth_design_doc(DDocId, VName), ..158 {ok, _Rev} = couch_db:update_doc(Db, AuthDesign, []), ..159 ?LOG_ERROR("created the design document", []), ..160 ok ..161 end. ..162 ..163 auth_design_doc(DocId, VName) -> ..164 DocProps = [ ..165 {<<"_id">>, DocId}, ..166 {<<"language">>,<<"javascript">>}, ..167 {<<"views">>, ..168 {[{VName, ..169 {[{<<"map">>, ..170 <<"function (doc) {\n if (doc.type == \"user\") {\n emit(doc.username, doc);\n}\n}">> ..171 }]} ..172 }]} ..173 }], ..174 {ok, couch_doc:from_json_obj({DocProps})}. ..175 ..176 ..177 user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles) -> ..178 user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles, nil). ..179 user_doc(DocId, Username, UserSalt, PasswordHash, Email, Active, Roles, Rev) -> ..180 DocProps = [ ..181 {<<"_id">>, DocId}, ..182 {<<"type">>, <<"user">>}, ..183 {<<"username">>, Username}, ..184 {<<"password_sha">>, PasswordHash}, ..185 {<<"salt">>, UserSalt}, ..186 {<<"email">>, Email}, ..187 {<<"active">>, Active}, ..188 {<<"roles">>, Roles}], ..189 DocProps1 = case Rev of ..190 nil -> DocProps; ..191 _Rev -> ..192 [{<<"_rev">>, Rev}] ++ DocProps ..193 end, ..194 {ok, couch_doc:from_json_obj({DocProps1})}. ..195 ..196 cookie_auth_user(_Req, undefined) -> nil; ..197 cookie_auth_user(#httpd{mochi_req=MochiReq}=Req, DbName) -> ..198 case MochiReq:get_cookie_value("AuthSession") of ..199 undefined -> nil; ..200 [] -> nil; ..201 Cookie -> ..202 case couch_db:open(DbName, [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of ..203 {ok, Db} -> ..204 try ..205 AuthSession = couch_util:decodeBase64Url(Cookie), ..206 [User, TimeStr | HashParts] = string:tokens(?b2l(AuthSession), ":"), ..207 % Verify expiry and hash ..208 {NowMS, NowS, _} = erlang:now(), ..209 CurrentTime = NowMS * 1000000 + NowS, ..210 case couch_config:get("couch_httpd_auth", "secret", nil) of ..211 nil -> nil; ..212 SecretStr -> ..213 Secret = ?l2b(SecretStr), ..214 case get_user(Db, ?l2b(User)) of ..215 nil -> nil; ..216 Result -> ..217 UserSalt = proplists:get_value(<<"salt">>, Result, <<"">>), ..218 FullSecret = <>, ..219 ExpectedHash = crypto:sha_mac(FullSecret, User ++ ":" ++ TimeStr), ..220 Hash = ?l2b(string:join(HashParts, ":")), ..221 Timeout = to_int(couch_config:get("couch_httpd_auth", "timeout", 600)), ..222 ?LOG_DEBUG("timeout ~p", [Timeout]), ..223 case (catch erlang:list_to_integer(TimeStr, 16)) of ..224 TimeStamp when CurrentTime < TimeStamp + Timeout ..225 andalso ExpectedHash == Hash -> ..226 TimeLeft = TimeStamp + Timeout - CurrentTime, ..227 ?LOG_DEBUG("Successful cookie auth as: ~p", [User]), ..228 Req#httpd{user_ctx=#user_ctx{ ..229 name=?l2b(User), ..230 roles=proplists:get_value(<<"roles">>, Result, []) ..231 }, auth={FullSecret, TimeLeft < Timeout*0.9}}; ..232 _Else -> ..233 nil ..234 end ..235 end ..236 end ..237 after ..238 couch_db:close(Db) ..239 end; ..240 _Else -> ..241 nil ..242 end ..243 end. ..244 ..245 cookie_auth_header(#httpd{user_ctx=#user_ctx{name=null}}, _Headers) -> []; ..246 cookie_auth_header(#httpd{user_ctx=#user_ctx{name=User}, auth={Secret, true}}, Headers) -> ..247 % Note: we only set the AuthSession cookie if: ..248 % * a valid AuthSession cookie has been received ..249 % * we are outside a 10% timeout window ..250 % * and if an AuthSession cookie hasn't already been set e.g. by a login ..251 % or logout handler. ..252 % The login and logout handlers need to set the AuthSession cookie ..253 % themselves. ..254 case proplists:get_value("Set-Cookie", Headers) of ..255 undefined -> []; ..256 Cookie -> ..257 case proplists:get_value("AuthSession", ..258 mochiweb_cookies:parse_cookie(Cookie), undefined) of ..259 undefined -> ..260 {NowMS, NowS, _} = erlang:now(), ..261 TimeStamp = NowMS * 1000000 + NowS, ..262 [cookie_auth_cookie(?b2l(User), Secret, TimeStamp)]; ..263 _Else -> [] ..264 end ..265 end; ..266 cookie_auth_header(_Req, _Headers) -> []. ..267 ..268 cookie_auth_cookie(User, Secret, TimeStamp) -> ..269 SessionData = User ++ ":" ++ erlang:integer_to_list(TimeStamp, 16), ..270 Hash = crypto:sha_mac(Secret, SessionData), ..271 mochiweb_cookies:cookie("AuthSession", ..272 couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)), ..273 [{path, "/"}, {http_only, true}]). % TODO add {secure, true} when SSL is detected ..274 ..275 % Login handler with user db ..276 handle_login_req(#httpd{method='POST', mochi_req=MochiReq}=Req, #db{}=Db) -> ..277 ReqBody = MochiReq:recv_body(), ..278 Form = case MochiReq:get_primary_header_value("content-type") of ..279 "application/x-www-form-urlencoded" ++ _ -> ..280 mochiweb_util:parse_qs(ReqBody); ..281 _ -> ..282 [] ..283 end, ..284 UserName = ?l2b(proplists:get_value("username", Form, "")), ..285 Password = ?l2b(proplists:get_value("password", Form, "")), ..286 User = case get_user(Db, UserName) of ..287 nil -> []; ..288 Result -> Result ..289 end, ..290 UserSalt = proplists:get_value(<<"salt">>, User, <<>>), ..291 PasswordHash = couch_util:encodeBase64(crypto:sha(<>)), ..292 case proplists:get_value(<<"password_sha">>, User, nil) of ..293 ExpectedHash when ExpectedHash == PasswordHash -> ..294 Secret = ?l2b(couch_config:get("couch_httpd_auth", "secret", nil)), ..295 {NowMS, NowS, _} = erlang:now(), ..296 CurrentTime = NowMS * 1000000 + NowS, ..297 Cookie = cookie_auth_cookie(?b2l(UserName), <>, CurrentTime), ..298 {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of ..299 nil -> ..300 {200, [Cookie]}; ..301 Redirect -> ..302 {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]} ..303 end, ..304 send_json(Req#httpd{req_body=ReqBody}, Code, Headers, ..305 {[{ok, true}]}); ..306 _Else -> ..307 throw({unauthorized, <<"Name or password is incorrect.">>}) ..308 end. ..309 ..310 % Session Handler ..311 ..312 handle_session_req(#httpd{method='POST'}=Req) -> ..313 % login ..314 DbName = couch_config:get("couch_httpd_auth", "authentication_db"), ..315 case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of ..316 {ok, Db} -> handle_login_req(Req, Db) ..317 end; ..318 handle_session_req(#httpd{method='GET', user_ctx=UserCtx}=Req) -> ..319 % whoami ..320 Name = UserCtx#user_ctx.name, ..321 Roles = UserCtx#user_ctx.roles, ..322 ForceLogin = couch_httpd:qs_value(Req, "basic", "false"), ..323 case {Name, ForceLogin} of ..324 {null, "true"} -> ..325 throw({unauthorized, <<"Please login.">>}); ..326 _False -> ok ..327 end, ..328 send_json(Req, {[ ..329 {ok, true}, ..330 {name, Name}, ..331 {roles, Roles} ..332 ]}); ..333 handle_session_req(#httpd{method='DELETE'}=Req) -> ..334 % logout ..335 Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}, {http_only, true}]), ..336 {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of ..337 nil -> ..338 {200, [Cookie]}; ..339 Redirect -> ..340 {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]} ..341 end, ..342 send_json(Req, Code, Headers, {[{ok, true}]}); ..343 handle_session_req(Req) -> ..344 send_method_not_allowed(Req, "GET,HEAD,POST,DELETE"). ..345 ..346 create_user_req(#httpd{method='POST', mochi_req=MochiReq}=Req, Db) -> ..347 ReqBody = MochiReq:recv_body(), ..348 Form = case MochiReq:get_primary_header_value("content-type") of ..349 "application/x-www-form-urlencoded" ++ _ -> ..350 ?LOG_INFO("body parsed ~p", [mochiweb_util:parse_qs(ReqBody)]), ..351 mochiweb_util:parse_qs(ReqBody); ..352 _ -> ..353 [] ..354 end, ..355 Roles = proplists:get_all_values("roles", Form), ..356 UserName = ?l2b(proplists:get_value("username", Form, "")), ..357 Password = ?l2b(proplists:get_value("password", Form, "")), ..358 Email = ?l2b(proplists:get_value("email", Form, "")), ..359 Active = couch_httpd_view:parse_bool_param(proplists:get_value("active", Form, "true")), ..360 case get_user(Db, UserName) of ..361 nil -> ..362 Roles1 = case Roles of ..363 [] -> Roles; ..364 _ -> ..365 ok = couch_httpd:verify_is_server_admin(Req), ..366 [?l2b(R) || R <- Roles] ..367 end, ..368 ..369 UserSalt = couch_util:new_uuid(), ..370 PasswordHash = couch_util:encodeBase64(crypto:sha(<>)), ..371 DocId = couch_util:new_uuid(), ..372 {ok, UserDoc} = user_doc(DocId, UserName, UserSalt, PasswordHash, Email, Active, Roles1), ..373 {ok, _Rev} = couch_db:update_doc(Db, UserDoc, []), ..374 ?LOG_DEBUG("User ~s (~s) with password, ~s created.", [?b2l(UserName), ?b2l(DocId), ?b2l(Password)]), ..375 {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of ..376 nil -> ..377 {200, []}; ..378 Redirect -> ..379 {302, [{"Location", couch_httpd:absolute_uri(Req, Redirect)}]} ..380 end, ..381 send_json(Req, Code, Headers, {[{ok, true}]}); ..382 _Result -> ..383 ?LOG_DEBUG("Can't create ~s: already exists", [?b2l(UserName)]), ..384 throw({forbidden, <<"User already exists.">>}) ..385 end. ..386 ..387 update_user_req(#httpd{method='PUT', mochi_req=MochiReq, user_ctx=UserCtx}=Req, Db, UserName) -> ..388 Name = UserCtx#user_ctx.name, ..389 UserRoles = UserCtx#user_ctx.roles, ..390 case User = get_user(Db, UserName) of ..391 nil -> ..392 throw({not_found, <<"User don't exist">>}); ..393 _Result -> ..394 ReqBody = MochiReq:recv_body(), ..395 Form = case MochiReq:get_primary_header_value("content-type") of ..396 "application/x-www-form-urlencoded" ++ _ -> ..397 mochiweb_util:parse_qs(ReqBody); ..398 _ -> ..399 [] ..400 end, ..401 Roles = proplists:get_all_values("roles", Form), ..402 Password = ?l2b(proplists:get_value("password", Form, "")), ..403 Email = ?l2b(proplists:get_value("email", Form, "")), ..404 Active = couch_httpd_view:parse_bool_param(proplists:get_value("active", Form, "true")), ..405 OldPassword = proplists:get_value("old_password", Form, ""), ..406 OldPassword1 = ?l2b(OldPassword), ..407 UserSalt = proplists:get_value(<<"salt">>, User, <<>>), ..408 OldRev = proplists:get_value(<<"_rev">>, User, <<>>), ..409 DocId = proplists:get_value(<<"_id">>, User, <<>>), ..410 CurrentPasswordHash = proplists:get_value(<<"password_sha">>, User, nil), ..411 ..412 ..413 Roles1 = case Roles of ..414 [] -> Roles; ..415 _ -> ..416 ok = couch_httpd:verify_is_server_admin(Req), ..417 [?l2b(R) || R <- Roles] ..418 end, ..419 ..420 PasswordHash = case lists:member(<<"_admin">>, UserRoles) of ..421 true -> ..422 Hash = case Password of ..423 <<>> -> CurrentPasswordHash; ..424 _Else -> ..425 H = couch_util:encodeBase64(crypto:sha(<>)), ..426 H ..427 end, ..428 Hash; ..429 false when Name == UserName -> ..430 %% for user we test old password before allowing change ..431 Hash = case Password of ..432 <<>> -> ..433 CurrentPasswordHash; ..434 _P when length(OldPassword) == 0 -> ..435 throw({forbidden, <<"Old password is incorrect.">>}); ..436 _Else -> ..437 OldPasswordHash = couch_util:encodeBase64(crypto:sha(<>)), ..438 ?LOG_DEBUG("~p == ~p", [CurrentPasswordHash, OldPasswordHash]), ..439 Hash1 = case CurrentPasswordHash of ..440 ExpectedHash when ExpectedHash == OldPasswordHash -> ..441 H = couch_util:encodeBase64(crypto:sha(<>)), ..442 H; ..443 _ -> ..444 throw({forbidden, <<"Old password is incorrect.">>}) ..445 end, ..446 Hash1 ..447 end, ..448 Hash; ..449 _ -> ..450 throw({forbidden, <<"You aren't allowed to change this password.">>}) ..451 end, ..452 {ok, UserDoc} = user_doc(DocId, UserName, UserSalt, PasswordHash, Email, Active, Roles1, OldRev), ..453 {ok, _Rev} = couch_db:update_doc(Db, UserDoc, []), ..454 ?LOG_DEBUG("User ~s (~s)updated.", [?b2l(UserName), ?b2l(DocId)]), ..455 {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of ..456 nil -> {200, []}; ..457 Redirect -> ..458 {302, [{"Location", couch_httpd:absolute_uri(Req, Redirect)}]} ..459 end, ..460 send_json(Req, Code, Headers, {[{ok, true}]}) ..461 end. ..462 ..463 handle_user_req(#httpd{method='POST'}=Req) -> ..464 DbName = couch_config:get("couch_httpd_auth", "authentication_db"), ..465 ensure_users_db_exists(?l2b(DbName)), ..466 case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of ..467 {ok, Db} -> create_user_req(Req, Db) ..468 end; ..469 handle_user_req(#httpd{method='PUT', path_parts=[_]}=_Req) -> ..470 throw({bad_request, <<"Username is missing">>}); ..471 handle_user_req(#httpd{method='PUT', path_parts=[_, UserName]}=Req) -> ..472 DbName = couch_config:get("couch_httpd_auth", "authentication_db"), ..473 ensure_users_db_exists(?l2b(DbName)), ..474 case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of ..475 {ok, Db} -> update_user_req(Req, Db, UserName) ..476 end; ..477 handle_user_req(Req) -> ..478 send_method_not_allowed(Req, "GET,HEAD,POST,PUT,DELETE"). ..479 ..480 to_int(Value) when is_binary(Value) -> ..481 to_int(?b2l(Value)); ..482 to_int(Value) when is_list(Value) -> ..483 erlang:list_to_integer(Value); ..484 to_int(Value) when is_integer(Value) -> ..485 Value. ..486 ..487 % % Login handler ..488 % handle_login_req(#httpd{method='POST'}=Req) -> ..489 % DbName = couch_config:get("couch_httpd_auth", "authentication_db"), ..490 % case couch_db:open(?l2b(DbName), [{user_ctx, #user_ctx{roles=[<<"_admin">>]}}]) of ..491 % {ok, Db} -> handle_login_req(Req, Db) ..492 % end; ..493 % handle_login_req(Req) -> ..494 % send_method_not_allowed(Req, "POST"). ..495 % ..496 % % Logout handler ..497 % handle_logout_req(#httpd{method='POST'}=Req) -> ..498 % Cookie = mochiweb_cookies:cookie("AuthSession", "", [{path, "/"}, {http_only, true}]), ..499 % {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of ..500 % nil -> ..501 % {200, [Cookie]}; ..502 % Redirect -> ..503 % {302, [Cookie, {"Location", couch_httpd:absolute_uri(Req, Redirect)}]} ..504 % end, ..505 % send_json(Req, Code, Headers, {[{ok, true}]}); ..506 % handle_logout_req(Req) -> ..507 % send_method_not_allowed(Req, "POST").
Generated using etap 0.3.4.