summaryrefslogtreecommitdiff
path: root/src/couchdb/couch_db.erl
diff options
context:
space:
mode:
authorJohn Christopher Anderson <jchris@apache.org>2010-02-01 22:51:15 +0000
committerJohn Christopher Anderson <jchris@apache.org>2010-02-01 22:51:15 +0000
commitee8a76e1cad33831448dbf12a394c51aa65230f4 (patch)
tree37e50fb2b43d4bb01b55fa8d1c05cda965b4dc4d /src/couchdb/couch_db.erl
parent8c381ee8de4c43e84f937584a2b3cd5923602057 (diff)
Database-level security.
This patch builds on the DB-admins feature to store lists of database admin and reader names and roles, as well as a security object which can be used for configuration in validation functions. git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@905436 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'src/couchdb/couch_db.erl')
-rw-r--r--src/couchdb/couch_db.erl132
1 files changed, 108 insertions, 24 deletions
diff --git a/src/couchdb/couch_db.erl b/src/couchdb/couch_db.erl
index 47b7c084..e3891821 100644
--- a/src/couchdb/couch_db.erl
+++ b/src/couchdb/couch_db.erl
@@ -22,7 +22,8 @@
-export([enum_docs/4,enum_docs_since/5]).
-export([enum_docs_since_reduce_to_count/1,enum_docs_reduce_to_count/1]).
-export([increment_update_seq/1,get_purge_seq/1,purge_docs/2,get_last_purged/1]).
--export([start_link/3,open_doc_int/3,set_admins/2,get_admins/1,ensure_full_commit/1]).
+-export([start_link/3,open_doc_int/3,ensure_full_commit/1]).
+-export([set_readers/2,get_readers/1,set_admins/2,get_admins/1,set_security/2,get_security/1]).
-export([init/1,terminate/2,handle_call/3,handle_cast/2,code_change/3,handle_info/2]).
-export([changes_since/5,changes_since/6,read_doc/2,new_revid/1]).
@@ -64,7 +65,18 @@ create(DbName, Options) ->
couch_server:create(DbName, Options).
open(DbName, Options) ->
- couch_server:open(DbName, Options).
+ case couch_server:open(DbName, Options) of
+ {ok, Db} ->
+ try
+ check_is_reader(Db),
+ {ok, Db}
+ catch
+ throw:Error ->
+ close(Db),
+ throw(Error)
+ end;
+ Else -> Else
+ end.
ensure_full_commit(#db{update_pid=UpdatePid,instance_start_time=StartTime}) ->
ok = gen_server:call(UpdatePid, full_commit, infinity),
@@ -218,22 +230,103 @@ get_design_docs(#db{fulldocinfo_by_id_btree=Btree}=Db) ->
[], [{start_key, <<"_design/">>}, {end_key_gt, <<"_design0">>}]),
{ok, Docs}.
-check_is_admin(#db{admins=Admins, user_ctx=#user_ctx{name=Name,roles=Roles}}) ->
- DbAdmins = [<<"_admin">> | Admins],
- case DbAdmins -- [Name | Roles] of
- DbAdmins -> % same list, not an admin
- throw({unauthorized, <<"You are not a db or server admin.">>});
+check_is_admin(#db{user_ctx=#user_ctx{name=Name,roles=Roles}}=Db) ->
+ {Admins} = get_admins(Db),
+ AdminRoles = [<<"_admin">> | proplists:get_value(roles, Admins, [])],
+ AdminNames = proplists:get_value(names, Admins,[]),
+ case AdminRoles -- Roles of
+ AdminRoles -> % same list, not an admin role
+ case AdminNames -- [Name] of
+ AdminNames -> % same names, not an admin
+ throw({unauthorized, <<"You are not a db or server admin.">>});
+ _ ->
+ ok
+ end;
_ ->
ok
end.
-get_admins(#db{admins=Admins}) ->
- Admins.
+check_is_reader(#db{user_ctx=#user_ctx{name=Name,roles=Roles}=UserCtx}=Db) ->
+ % admins are not readers. this is for good reason.
+ % we don't want to confuse setting admins with making private dbs
+ {Readers} = get_readers(Db),
+ ReaderRoles = proplists:get_value(roles, Readers,[]),
+ WithAdminRoles = [<<"_admin">> | ReaderRoles],
+ ReaderNames = proplists:get_value(names, Readers,[]),
+ case ReaderRoles ++ ReaderNames of
+ [] -> ok; % no readers == public access
+ _Else ->
+ case WithAdminRoles -- Roles of
+ WithAdminRoles -> % same list, not an reader role
+ case ReaderNames -- [Name] of
+ ReaderNames -> % same names, not a reader
+ ?LOG_DEBUG("Not a reader: UserCtx ~p vs Names ~p Roles ~p",[UserCtx, ReaderNames, WithAdminRoles]),
+ throw({unauthorized, <<"You are not authorized to access this db.">>});
+ _ ->
+ ok
+ end;
+ _ ->
+ ok
+ end
+ end.
+
+get_admins(#db{security=SecProps}) ->
+ proplists:get_value(admins, SecProps, {[]}).
+
+set_admins(#db{security=SecProps,update_pid=Pid}=Db, Admins) ->
+ check_is_admin(Db),
+ SecProps2 = update_sec_field(admins, SecProps, just_names_and_roles(Admins)),
+ gen_server:call(Pid, {set_security, SecProps2}, infinity).
+
+get_readers(#db{security=SecProps}) ->
+ proplists:get_value(readers, SecProps, {[]}).
+
+set_readers(#db{security=SecProps,update_pid=Pid}=Db, Readers) ->
+ check_is_admin(Db),
+ SecProps2 = update_sec_field(readers, SecProps, just_names_and_roles(Readers)),
+ gen_server:call(Pid, {set_security, SecProps2}, infinity).
+
+get_security(#db{security=SecProps}) ->
+ proplists:get_value(sec_obj, SecProps, {[]}).
-set_admins(#db{update_pid=Pid}=Db, Admins) when is_list(Admins) ->
+set_security(#db{security=SecProps, update_pid=Pid}=Db, {SecObjProps}) when is_list(SecObjProps) ->
check_is_admin(Db),
- gen_server:call(Pid, {set_admins, Admins}, infinity).
+ SecProps2 = update_sec_field(sec_obj, SecProps, {SecObjProps}),
+ gen_server:call(Pid, {set_security, SecProps2}, infinity).
+
+update_sec_field(Field, SecProps, Value) ->
+ Admins = proplists:get_value(admins, SecProps, {[]}),
+ Readers = proplists:get_value(readers, SecProps, {[]}),
+ SecObj = proplists:get_value(sec_obj, SecProps, {[]}),
+ if Field == admins ->
+ [{admins, Value}];
+ true -> [{admins, Admins}]
+ end ++ if Field == readers ->
+ [{readers, Value}];
+ true -> [{readers, Readers}]
+ end ++ if Field == sec_obj ->
+ [{sec_obj, Value}];
+ true -> [{sec_obj, SecObj}]
+ end.
+% validate user input and convert proplist to atom keys
+just_names_and_roles({Props}) when is_list(Props) ->
+ Names = case proplists:get_value(<<"names">>,Props) of
+ Ns when is_list(Ns) ->
+ [throw("names must be a JSON list of strings") ||N <- Ns, not is_binary(N)],
+ Ns;
+ _ -> []
+ end,
+ Roles = case proplists:get_value(<<"roles">>,Props) of
+ Rs when is_list(Rs) ->
+ [throw("roles must be a JSON list of strings") ||R <- Rs, not is_binary(R)],
+ Rs;
+ _ -> []
+ end,
+ {[
+ {names, Names},
+ {roles, Roles}
+ ]}.
get_revs_limit(#db{revs_limit=Limit}) ->
Limit.
@@ -282,18 +375,8 @@ group_alike_docs([Doc|Rest], [Bucket|RestBuckets]) ->
group_alike_docs(Rest, [[Doc]|[Bucket|RestBuckets]])
end.
-
-validate_doc_update(#db{user_ctx=UserCtx, admins=Admins},
- #doc{id= <<"_design/",_/binary>>}, _GetDiskDocFun) ->
- UserNames = [UserCtx#user_ctx.name | UserCtx#user_ctx.roles],
- % if the user is a server admin or db admin, allow the save
- case length(UserNames -- [<<"_admin">> | Admins]) =:= length(UserNames) of
- true ->
- % not an admin
- {unauthorized, <<"You are not a server or database admin.">>};
- false ->
- ok
- end;
+validate_doc_update(#db{}=Db, #doc{id= <<"_design/",_/binary>>}, _GetDiskDocFun) ->
+ catch check_is_admin(Db);
validate_doc_update(#db{validate_doc_funs=[]}, _Doc, _GetDiskDocFun) ->
ok;
validate_doc_update(_Db, #doc{id= <<"_local/",_/binary>>}, _GetDiskDocFun) ->
@@ -301,7 +384,8 @@ validate_doc_update(_Db, #doc{id= <<"_local/",_/binary>>}, _GetDiskDocFun) ->
validate_doc_update(Db, Doc, GetDiskDocFun) ->
DiskDoc = GetDiskDocFun(),
JsonCtx = couch_util:json_user_ctx(Db),
- try [case Fun(Doc, DiskDoc, JsonCtx) of
+ SecObj = get_security(Db),
+ try [case Fun(Doc, DiskDoc, JsonCtx, SecObj) of
ok -> ok;
Error -> throw(Error)
end || Fun <- Db#db.validate_doc_funs],