From 50decd4044bb8fcfd22c485e064a11d63fa15a2d Mon Sep 17 00:00:00 2001 From: "Damien F. Katz" Date: Wed, 22 Oct 2008 03:08:53 +0000 Subject: First check-in of admin http authentication and authorization. git-svn-id: https://svn.apache.org/repos/asf/incubator/couchdb/trunk@706848 13f79535-47bb-0310-9956-ffa450edef68 --- src/couchdb/couch_db.hrl | 3 +++ src/couchdb/couch_httpd.erl | 33 ++++++++++++++++++++++++++++++- src/couchdb/couch_httpd_db.erl | 2 ++ src/couchdb/couch_httpd_misc_handlers.erl | 6 ++++++ src/couchdb/couch_server.erl | 29 ++++++++++++++++++++++++++- 5 files changed, 71 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/couchdb/couch_db.hrl b/src/couchdb/couch_db.hrl index bf98e182..d1825d9e 100644 --- a/src/couchdb/couch_db.hrl +++ b/src/couchdb/couch_db.hrl @@ -17,6 +17,9 @@ -define(JSON_ENCODE(V), mochijson2:encode(V)). -define(JSON_DECODE(V), mochijson2:decode(V)). +-define(b2l(V), binary_to_list(V)). +-define(l2b(V), list_to_binary(V)). + -define(DEFAULT_ATTACHMENT_CONTENT_TYPE, <<"application/octet-stream">>). -define(LOG_DEBUG(Format, Args), diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index 7cc5546f..2be90893 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -15,7 +15,8 @@ -export([start_link/0, stop/0, handle_request/3]). --export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,path/1,unquote/1]). +-export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,path/1]). +-export([check_is_admin/1,unquote/1]). -export([parse_form/1,json_body/1,body/1,doc_etag/1]). -export([primary_header_value/2,partition/1,serve_file/3]). -export([start_chunked_response/3,send_chunk/2]). @@ -85,6 +86,7 @@ start_link() -> stop() -> mochiweb_http:stop(?MODULE). + handle_request(MochiReq, UrlHandlers, DbUrlHandlers) -> @@ -195,6 +197,30 @@ json_body(#httpd{mochi_req=MochiReq}) -> doc_etag(#doc{revs=[DiskRev|_]}) -> "\"" ++ binary_to_list(DiskRev) ++ "\"". +check_is_admin(Req) -> + IsNamedAdmin = + case header_value(Req, "Authorization") of + "Basic " ++ Base64Value -> + [User, Pass] = + string:tokens(?b2l(couch_util:decodeBase64(Base64Value)),":"), + couch_server:is_admin(User, Pass); + _ -> + false + end, + + case IsNamedAdmin of + true -> + ok; + false -> + case couch_server:has_admins() of + true -> + throw(admin_auth_error); + false -> + % if no admins, then everyone is admin! Yay, admin party! + ok + end + end. + start_chunked_response(#httpd{mochi_req=MochiReq}, Code, Headers) -> {ok, MochiReq:respond({Code, Headers ++ server_header(), chunked})}. @@ -250,6 +276,11 @@ send_error(Req, {not_found, Reason}) -> send_error(Req, 404, <<"not_found">>, Reason); send_error(Req, conflict) -> send_error(Req, 412, <<"conflict">>, <<"Document update conflict.">>); +send_error(Req, admin_auth_error) -> + send_json(Req, 401, + [{"WWW-Authenticate", "Basic realm=\"admin\""}], + {[{<<"error">>, <<"auth_error">>}, + {<<"reason">>, <<"Admin user name and password required">>}]}); send_error(Req, {doc_validation, Msg}) -> send_error(Req, 406, <<"doc_validation">>, Msg); send_error(Req, file_exists) -> diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index a60686fd..8b8a7df0 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -42,6 +42,7 @@ handle_request(#httpd{path_parts=[DbName|RestParts],method=Method, end. create_db_req(Req, DbName) -> + ok = couch_httpd:check_is_admin(Req), case couch_server:create(DbName, []) of {ok, Db} -> couch_db:close(Db), @@ -51,6 +52,7 @@ create_db_req(Req, DbName) -> end. delete_db_req(Req, DbName) -> + ok = couch_httpd:check_is_admin(Req), case couch_server:delete(DbName) of ok -> send_json(Req, 200, {[{ok, true}]}); diff --git a/src/couchdb/couch_httpd_misc_handlers.erl b/src/couchdb/couch_httpd_misc_handlers.erl index 8372c1b5..2d2434e1 100644 --- a/src/couchdb/couch_httpd_misc_handlers.erl +++ b/src/couchdb/couch_httpd_misc_handlers.erl @@ -70,6 +70,7 @@ handle_replicate_req(Req) -> handle_restart_req(#httpd{method='POST'}=Req) -> + ok = couch_httpd:check_is_admin(Req), Response = send_json(Req, {[{ok, true}]}), spawn(fun() -> couch_server:remote_restart() end), Response; @@ -93,6 +94,7 @@ handle_uuids_req(Req) -> % GET /_config/ % GET /_config handle_config_req(#httpd{method='GET', path_parts=[_]}=Req) -> + ok = couch_httpd:check_is_admin(Req), Grouped = lists:foldl(fun({{Section, Key}, Value}, Acc) -> case dict:is_key(Section, Acc) of true -> @@ -107,12 +109,14 @@ handle_config_req(#httpd{method='GET', path_parts=[_]}=Req) -> send_json(Req, 200, {KVs}); % GET /_config/Section handle_config_req(#httpd{method='GET', path_parts=[_,Section]}=Req) -> + ok = couch_httpd:check_is_admin(Req), KVs = [{list_to_binary(Key), list_to_binary(Value)} || {Key, Value} <- couch_config:get(Section)], send_json(Req, 200, {KVs}); % PUT /_config/Section/Key % "value" handle_config_req(#httpd{method='PUT', path_parts=[_, Section, Key]}=Req) -> + ok = couch_httpd:check_is_admin(Req), Value = binary_to_list(couch_httpd:body(Req)), ok = couch_config:set(Section, Key, Value), send_json(Req, 200, {[ @@ -120,6 +124,7 @@ handle_config_req(#httpd{method='PUT', path_parts=[_, Section, Key]}=Req) -> ]}); % GET /_config/Section/Key handle_config_req(#httpd{method='GET', path_parts=[_, Section, Key]}=Req) -> + ok = couch_httpd:check_is_admin(Req), case couch_config:get(Section, Key, null) of null -> throw({not_found, unknown_config_value}); @@ -128,6 +133,7 @@ handle_config_req(#httpd{method='GET', path_parts=[_, Section, Key]}=Req) -> end; % DELETE /_config/Section/Key handle_config_req(#httpd{method='DELETE',path_parts=[_,Section,Key]}=Req) -> + ok = couch_httpd:check_is_admin(Req), case couch_config:get(Section, Key, null) of null -> throw({not_found, unknown_config_value}); diff --git a/src/couchdb/couch_server.erl b/src/couchdb/couch_server.erl index 95d51fc7..8363d4a1 100644 --- a/src/couchdb/couch_server.erl +++ b/src/couchdb/couch_server.erl @@ -18,7 +18,7 @@ -export([open/2,create/2,delete/1,all_databases/0,get_version/0]). -export([init/1, handle_call/3,sup_start_link/0]). -export([handle_cast/2,code_change/3,handle_info/2,terminate/2]). --export([dev_start/0,remote_restart/0]). +-export([dev_start/0,remote_restart/0,is_admin/2,has_admins/0]). -include("couch_db.hrl"). @@ -85,9 +85,31 @@ check_dbname(#server{dbname_regexp=RegExp}, DbName) -> ok end. +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. + +has_admins() -> + couch_config:get("admins") /= []. + get_full_filename(Server, DbName) -> filename:join([Server#server.root_dir, "./" ++ DbName ++ ".couch"]). +hash_admin_passwords() -> + lists:foreach( + fun({_User, "-hashed-" ++ _}) -> + ok; % already hashed + ({User, ClearPassword}) -> + Salt = ?b2l(couch_util:new_uuid()), + Hashed = couch_util:to_hex(crypto:sha(ClearPassword ++ Salt)), + couch_config:set("admins", User, "-hashed-" ++ Hashed ++ "," ++ Salt) + end, couch_config:get("admins")). + init([]) -> % read config and register for configuration changes @@ -103,6 +125,11 @@ init([]) -> ("couchdb", "server_options") -> exit(Self, config_change) end), + ok = couch_config:register( + fun("admins", _) -> + hash_admin_passwords() + end), + hash_admin_passwords(), {ok, RegExp} = regexp:parse("^[a-z][a-z0-9\\_\\$()\\+\\-\\/]*$"), ets:new(couch_dbs_by_name, [set, private, named_table]), ets:new(couch_dbs_by_pid, [set, private, named_table]), -- cgit v1.2.3