summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/couchdb/couch_db.hrl3
-rw-r--r--src/couchdb/couch_httpd.erl33
-rw-r--r--src/couchdb/couch_httpd_db.erl2
-rw-r--r--src/couchdb/couch_httpd_misc_handlers.erl6
-rw-r--r--src/couchdb/couch_server.erl29
5 files changed, 71 insertions, 2 deletions
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]),