C0 code coverage information

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 ?? ?? ??
4% 
....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.