summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Vatamaniuc <vatamane@apache.org>2019-09-19 16:42:35 -0400
committerNick Vatamaniuc <nickva@users.noreply.github.com>2019-09-19 18:52:29 -0400
commit2051bd6e610a09d40cd86c1e0694d33be58dc26e (patch)
tree5d23534b62ecb0aaeaf37e9097bbe026e02502ab
parent9c6b5cbd3fdebb45162df57c59ed9291ab5138fd (diff)
downloadcouchdb-2051bd6e610a09d40cd86c1e0694d33be58dc26e.tar.gz
Check members after db is opened
This brings this to parity with master `couch_db:open/2` logic: https://github.com/apache/couchdb/blob/master/src/couch/src/couch_db.erl#L166 There are two separate cases that have to be handled: 1) Db was already opened and cached. In that case, call `check_is_member(Db)` which reads the security from the Db to ensure we don't authorize against a stale security doc. Otherwise, the delay could be as long as the last write that went through that node. A potential future optimization could be to have a timestamp and only get the new security context if last refresh hasn't happened recently. 2) Db was not cached, and was just opened. To avoid running another two read transactions to get the security doc after the main transaction finished, call a version of check_is_member which gets the security doc passed in as an argument. As a bonus, `check_is_members(Db, SecDoc)` version ends up saving one extra security read since we don't read twice in is_member and is_admin calls. `delete/2` was updated to pass ?ADMIN_CTX to `open/2` since it only cares about getting a `database_does_not_exist` error thrown. There is a check for server admin at the HTTP API level that would care of authentication / authorization.
-rw-r--r--src/fabric/src/fabric2_db.erl29
-rw-r--r--src/fabric/test/fabric2_db_security_tests.erl26
2 files changed, 42 insertions, 13 deletions
diff --git a/src/fabric/src/fabric2_db.erl b/src/fabric/src/fabric2_db.erl
index 8927ce365..a316517eb 100644
--- a/src/fabric/src/fabric2_db.erl
+++ b/src/fabric/src/fabric2_db.erl
@@ -175,14 +175,17 @@ create(DbName, Options) ->
open(DbName, Options) ->
case fabric2_server:fetch(DbName) of
#{} = Db ->
- {ok, maybe_set_user_ctx(Db, Options)};
+ Db1 = maybe_set_user_ctx(Db, Options),
+ ok = check_is_member(Db1),
+ {ok, Db1};
undefined ->
Result = fabric2_fdb:transactional(DbName, Options, fun(TxDb) ->
fabric2_fdb:open(TxDb, Options)
end),
% Cache outside the transaction retry loop
case Result of
- #{} = Db0 ->
+ #{security_doc := SecDoc} = Db0 ->
+ ok = check_is_member(Db0, SecDoc),
Db1 = maybe_add_sys_db_callbacks(Db0),
ok = fabric2_server:store(Db1),
{ok, Db1#{tx := undefined}};
@@ -193,8 +196,10 @@ open(DbName, Options) ->
delete(DbName, Options) ->
- % This will throw if the db does not exist
- {ok, Db} = open(DbName, Options),
+ % Delete doesn't check user_ctx, that's done at the HTTP API level
+ % here we just care to get the `database_does_not_exist` error thrown
+ Options1 = lists:keystore(user_ctx, 1, Options, ?ADMIN_CTX),
+ {ok, Db} = open(DbName, Options1),
Resp = fabric2_fdb:transactional(Db, fun(TxDb) ->
fabric2_fdb:delete(TxDb)
end),
@@ -236,11 +241,14 @@ list_dbs(UserFun, UserAcc0, Options) ->
is_admin(Db) ->
+ is_admin(Db, get_security(Db)).
+
+
+is_admin(Db, {SecProps}) when is_list(SecProps) ->
case fabric2_db_plugin:check_is_admin(Db) of
true ->
true;
false ->
- {SecProps} = get_security(Db),
UserCtx = get_user_ctx(Db),
{Admins} = get_admins(SecProps),
is_authorized(Admins, UserCtx)
@@ -259,7 +267,11 @@ check_is_admin(Db) ->
check_is_member(Db) ->
- case is_member(Db) of
+ check_is_member(Db, get_security(Db)).
+
+
+check_is_member(Db, SecDoc) ->
+ case is_member(Db, SecDoc) of
true ->
ok;
false ->
@@ -977,9 +989,8 @@ maybe_set_user_ctx(Db, Options) ->
end.
-is_member(Db) ->
- {SecProps} = get_security(Db),
- case is_admin(Db) of
+is_member(Db, {SecProps}) when is_list(SecProps) ->
+ case is_admin(Db, {SecProps}) of
true ->
true;
false ->
diff --git a/src/fabric/test/fabric2_db_security_tests.erl b/src/fabric/test/fabric2_db_security_tests.erl
index b4df3b4dd..4a54083ac 100644
--- a/src/fabric/test/fabric2_db_security_tests.erl
+++ b/src/fabric/test/fabric2_db_security_tests.erl
@@ -38,7 +38,10 @@ security_test_() ->
fun check_is_not_member_role/1,
fun check_admin_is_member/1,
fun check_is_member_of_public_db/1,
- fun check_set_user_ctx/1
+ fun check_set_user_ctx/1,
+ fun check_open_forbidden/1,
+ fun check_fail_open_no_opts/1,
+ fun check_fail_open_name_null/1
]}
}
}.
@@ -48,7 +51,7 @@ setup() ->
Ctx = test_util:start_couch([fabric]),
DbName = ?tempdb(),
PubDbName = ?tempdb(),
- {ok, Db1} = fabric2_db:create(DbName, [{user_ctx, ?ADMIN_USER}]),
+ {ok, Db1} = fabric2_db:create(DbName, [?ADMIN_CTX]),
SecProps = {[
{<<"admins">>, {[
{<<"names">>, [<<"admin_name1">>, <<"admin_name2">>]},
@@ -60,7 +63,7 @@ setup() ->
]}}
]},
ok = fabric2_db:set_security(Db1, SecProps),
- {ok, Db2} = fabric2_db:open(DbName, []),
+ {ok, Db2} = fabric2_db:open(DbName, [?ADMIN_CTX]),
{ok, PubDb} = fabric2_db:create(PubDbName, []),
{Db2, PubDb, Ctx}.
@@ -157,8 +160,23 @@ check_is_member_of_public_db({_, PubDb, _}) ->
check_set_user_ctx({Db0, _, _}) ->
DbName = fabric2_db:name(Db0),
- UserCtx = #user_ctx{name = <<"foo">>, roles = [<<"bar">>]},
+ UserCtx = #user_ctx{name = <<"foo">>, roles = [<<"admin_role1">>]},
{ok, Db1} = fabric2_db:open(DbName, [{user_ctx, UserCtx}]),
?assertEqual(UserCtx, fabric2_db:get_user_ctx(Db1)).
+check_open_forbidden({Db0, _, _}) ->
+ DbName = fabric2_db:name(Db0),
+ UserCtx = #user_ctx{name = <<"foo">>, roles = [<<"bar">>]},
+ ?assertThrow({forbidden, _}, fabric2_db:open(DbName, [{user_ctx, UserCtx}])).
+
+
+check_fail_open_no_opts({Db0, _, _}) ->
+ DbName = fabric2_db:name(Db0),
+ ?assertThrow({unauthorized, _}, fabric2_db:open(DbName, [])).
+
+
+check_fail_open_name_null({Db0, _, _}) ->
+ DbName = fabric2_db:name(Db0),
+ UserCtx = #user_ctx{name = null},
+ ?assertThrow({unauthorized, _}, fabric2_db:open(DbName, [{user_ctx, UserCtx}])).