Repository: couchdb-peruser Updated Branches: refs/heads/master 7530ed1d7 -> 27f4799ab
Rename application to 'couch_peruser' Project: http://git-wip-us.apache.org/repos/asf/couchdb-peruser/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-peruser/commit/27f4799a Tree: http://git-wip-us.apache.org/repos/asf/couchdb-peruser/tree/27f4799a Diff: http://git-wip-us.apache.org/repos/asf/couchdb-peruser/diff/27f4799a Branch: refs/heads/master Commit: 27f4799ab76d272919a75eace63b2e5df6c726de Parents: 7530ed1 Author: Klaus Trainer <[email protected]> Authored: Thu Aug 20 14:56:43 2015 +0200 Committer: Klaus Trainer <[email protected]> Committed: Thu Aug 20 14:56:43 2015 +0200 ---------------------------------------------------------------------- README.md | 4 +- src/couch_peruser.app.src | 18 +++ src/couch_peruser.erl | 215 ++++++++++++++++++++++++++++ src/couchdb_peruser.app.src | 18 --- src/couchdb_peruser.erl | 215 ---------------------------- test/couch_peruser_test.erl | 279 +++++++++++++++++++++++++++++++++++++ test/couchdb_peruser_test.erl | 279 ------------------------------------- 7 files changed, 514 insertions(+), 514 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-peruser/blob/27f4799a/README.md ---------------------------------------------------------------------- diff --git a/README.md b/README.md index 4ada11c..f33841d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -couchdb_peruser +couch_peruser =============== -couchdb_peruser is a CouchDB daemon that ensures that a private per-user +couch_peruser is a CouchDB daemon that ensures that a private per-user database exists for each document in _users. These databases are writable only by the corresponding user. Databases are in the form: http://git-wip-us.apache.org/repos/asf/couchdb-peruser/blob/27f4799a/src/couch_peruser.app.src ---------------------------------------------------------------------- diff --git a/src/couch_peruser.app.src b/src/couch_peruser.app.src new file mode 100644 index 0000000..fb6d45b --- /dev/null +++ b/src/couch_peruser.app.src @@ -0,0 +1,18 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +{application, couch_peruser, [ + {description, "couch_peruser - maintains per-user databases in CouchDB"}, + {vsn, git}, + {registered, []}, + {applications, [kernel, stdlib, config, couch, fabric]} +]}. http://git-wip-us.apache.org/repos/asf/couchdb-peruser/blob/27f4799a/src/couch_peruser.erl ---------------------------------------------------------------------- diff --git a/src/couch_peruser.erl b/src/couch_peruser.erl new file mode 100644 index 0000000..f20b979 --- /dev/null +++ b/src/couch_peruser.erl @@ -0,0 +1,215 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_peruser). +-behaviour(gen_server). +-behaviour(config_listener). + +-include_lib("couch/include/couch_db.hrl"). + +-define(USERDB_PREFIX, "userdb-"). + +% gen_server callbacks +-export([start_link/0, init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +% config_listener callbacks +-export([handle_config_change/5, handle_config_terminate/3]). + +-export([init_changes_handler/1, changes_handler/3]). + +-record(state, {parent, db_name, delete_dbs, changes_pid, changes_ref}). + + +start_link() -> + gen_server:start_link(?MODULE, [], []). + +init() -> + case config:get_boolean("couch_peruser", "enable", false) of + false -> + #state{}; + true -> + DbName = ?l2b(config:get( + "couch_httpd_auth", "authentication_db", "_users")), + DeleteDbs = config:get_boolean("couch_peruser", "delete_dbs", false), + State = #state{parent = self(), + db_name = DbName, + delete_dbs = DeleteDbs}, + {Pid, Ref} = spawn_opt( + ?MODULE, init_changes_handler, [State], [link, monitor]), + State#state{changes_pid=Pid, changes_ref=Ref} + end. + +init_changes_handler(#state{db_name=DbName} = State) -> + try + {ok, Db} = couch_db:open_int(DbName, [?ADMIN_CTX, sys_db]), + FunAcc = {fun ?MODULE:changes_handler/3, State}, + (couch_changes:handle_db_changes( + #changes_args{feed="continuous", timeout=infinity}, + {json_req, null}, + Db))(FunAcc) + catch error:database_does_not_exist -> + ok + end. + +changes_handler({change, {Doc}, _Prepend}, _ResType, State=#state{}) -> + case couch_util:get_value(<<"id">>, Doc) of + <<"org.couchdb.user:",User/binary>> -> + case couch_util:get_value(<<"deleted">>, Doc, false) of + false -> + UserDb = ensure_user_db(User), + ok = ensure_security(User, UserDb, fun add_user/3), + State; + true -> + case State#state.delete_dbs of + true -> + _UserDb = delete_user_db(User), + State; + false -> + UserDb = user_db_name(User), + ok = ensure_security(User, UserDb, fun remove_user/3), + State + end + end; + _ -> + State + end; +changes_handler(_Event, _ResType, State) -> + State. + +delete_user_db(User) -> + UserDb = user_db_name(User), + try + case fabric:delete_db(UserDb, [?ADMIN_CTX]) of + ok -> ok; + accepted -> ok + end + catch error:database_does_not_exist -> + ok + end, + UserDb. + +ensure_user_db(User) -> + UserDb = user_db_name(User), + try + {ok, _DbInfo} = fabric:get_db_info(UserDb) + catch error:database_does_not_exist -> + case fabric:create_db(UserDb, [?ADMIN_CTX]) of + ok -> ok; + accepted -> ok + end + end, + UserDb. + +add_user(User, Prop, {Modified, SecProps}) -> + {PropValue} = couch_util:get_value(Prop, SecProps, {[]}), + Names = couch_util:get_value(<<"names">>, PropValue, []), + case lists:member(User, Names) of + true -> + {Modified, SecProps}; + false -> + {true, + lists:keystore( + Prop, 1, SecProps, + {Prop, + {lists:keystore( + <<"names">>, 1, PropValue, + {<<"names">>, [User | Names]})}})} + end. + +remove_user(User, Prop, {Modified, SecProps}) -> + {PropValue} = couch_util:get_value(Prop, SecProps, {[]}), + Names = couch_util:get_value(<<"names">>, PropValue, []), + case lists:member(User, Names) of + false -> + {Modified, SecProps}; + true -> + {true, + lists:keystore( + Prop, 1, SecProps, + {Prop, + {lists:keystore( + <<"names">>, 1, PropValue, + {<<"names">>, lists:delete(User, Names)})}})} + end. + +ensure_security(User, UserDb, TransformFun) -> + {ok, Shards} = fabric:get_all_security(UserDb, [?ADMIN_CTX]), + {_ShardInfo, {SecProps}} = hd(Shards), + % assert that shards have the same security object + true = lists:all(fun ({_, {SecProps1}}) -> + SecProps =:= SecProps1 + end, Shards), + case lists:foldl( + fun (Prop, SAcc) -> TransformFun(User, Prop, SAcc) end, + {false, SecProps}, + [<<"admins">>, <<"members">>]) of + {false, _} -> + ok; + {true, SecProps1} -> + ok = fabric:set_security(UserDb, {SecProps1}, [?ADMIN_CTX]) + end. + +user_db_name(User) -> + HexUser = list_to_binary( + [string:to_lower(integer_to_list(X, 16)) || <<X>> <= User]), + <<?USERDB_PREFIX,HexUser/binary>>. + + +%% gen_server callbacks + +init([]) -> + ok = config:listen_for_changes(?MODULE, self()), + {ok, init()}. + +handle_call(_Msg, _From, State) -> + {reply, error, State}. + +handle_cast(update_config, State) when State#state.changes_pid =/= undefined -> + % we don't want to have multiple changes handler at the same time + demonitor(State#state.changes_ref, [flush]), + exit(State#state.changes_pid, kill), + {noreply, init()}; +handle_cast(update_config, _State) -> + {noreply, init()}; +handle_cast(stop, State) -> + {stop, normal, State}; +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info({'DOWN', Ref, _, _, _Reason}, #state{changes_ref=Ref} = State) -> + {stop, normal, State}; +handle_info(_Msg, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + %% Everything should be linked or monitored, let nature + %% take its course. + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +%% config_listener callbacks + +handle_config_change("couch_httpd_auth", "authentication_db", _Value, _Persist, Server) -> + ok = gen_server:cast(Server, update_config), + {ok, Server}; +handle_config_change("couch_peruser", _Key, _Value, _Persist, Server) -> + ok = gen_server:cast(Server, update_config), + {ok, Server}; +handle_config_change(_Section, _Key, _Value, _Persist, Server) -> + {ok, Server}. + +handle_config_terminate(_Self, Reason, _Server) -> + {stop, Reason}. http://git-wip-us.apache.org/repos/asf/couchdb-peruser/blob/27f4799a/src/couchdb_peruser.app.src ---------------------------------------------------------------------- diff --git a/src/couchdb_peruser.app.src b/src/couchdb_peruser.app.src deleted file mode 100644 index 0bbf0cf..0000000 --- a/src/couchdb_peruser.app.src +++ /dev/null @@ -1,18 +0,0 @@ -% Licensed under the Apache License, Version 2.0 (the "License"); you may not -% use this file except in compliance with the License. You may obtain a copy of -% the License at -% -% http://www.apache.org/licenses/LICENSE-2.0 -% -% Unless required by applicable law or agreed to in writing, software -% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -% License for the specific language governing permissions and limitations under -% the License. - -{application, couchdb_peruser, [ - {description, "couchdb_peruser - maintains per-user databases in CouchDB"}, - {vsn, git}, - {registered, []}, - {applications, [kernel, stdlib, config, couch, fabric]} -]}. http://git-wip-us.apache.org/repos/asf/couchdb-peruser/blob/27f4799a/src/couchdb_peruser.erl ---------------------------------------------------------------------- diff --git a/src/couchdb_peruser.erl b/src/couchdb_peruser.erl deleted file mode 100644 index a4c8a2e..0000000 --- a/src/couchdb_peruser.erl +++ /dev/null @@ -1,215 +0,0 @@ -% Licensed under the Apache License, Version 2.0 (the "License"); you may not -% use this file except in compliance with the License. You may obtain a copy of -% the License at -% -% http://www.apache.org/licenses/LICENSE-2.0 -% -% Unless required by applicable law or agreed to in writing, software -% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -% License for the specific language governing permissions and limitations under -% the License. - --module(couchdb_peruser). --behaviour(gen_server). --behaviour(config_listener). - --include_lib("couch/include/couch_db.hrl"). - --define(USERDB_PREFIX, "userdb-"). - -% gen_server callbacks --export([start_link/0, init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - -% config_listener callbacks --export([handle_config_change/5, handle_config_terminate/3]). - --export([init_changes_handler/1, changes_handler/3]). - --record(state, {parent, db_name, delete_dbs, changes_pid, changes_ref}). - - -start_link() -> - gen_server:start_link(?MODULE, [], []). - -init() -> - case config:get_boolean("couchdb_peruser", "enable", false) of - false -> - #state{}; - true -> - DbName = ?l2b(config:get( - "couch_httpd_auth", "authentication_db", "_users")), - DeleteDbs = config:get_boolean("couchdb_peruser", "delete_dbs", false), - State = #state{parent = self(), - db_name = DbName, - delete_dbs = DeleteDbs}, - {Pid, Ref} = spawn_opt( - ?MODULE, init_changes_handler, [State], [link, monitor]), - State#state{changes_pid=Pid, changes_ref=Ref} - end. - -init_changes_handler(#state{db_name=DbName} = State) -> - try - {ok, Db} = couch_db:open_int(DbName, [?ADMIN_CTX, sys_db]), - FunAcc = {fun ?MODULE:changes_handler/3, State}, - (couch_changes:handle_db_changes( - #changes_args{feed="continuous", timeout=infinity}, - {json_req, null}, - Db))(FunAcc) - catch error:database_does_not_exist -> - ok - end. - -changes_handler({change, {Doc}, _Prepend}, _ResType, State=#state{}) -> - case couch_util:get_value(<<"id">>, Doc) of - <<"org.couchdb.user:",User/binary>> -> - case couch_util:get_value(<<"deleted">>, Doc, false) of - false -> - UserDb = ensure_user_db(User), - ok = ensure_security(User, UserDb, fun add_user/3), - State; - true -> - case State#state.delete_dbs of - true -> - _UserDb = delete_user_db(User), - State; - false -> - UserDb = user_db_name(User), - ok = ensure_security(User, UserDb, fun remove_user/3), - State - end - end; - _ -> - State - end; -changes_handler(_Event, _ResType, State) -> - State. - -delete_user_db(User) -> - UserDb = user_db_name(User), - try - case fabric:delete_db(UserDb, [?ADMIN_CTX]) of - ok -> ok; - accepted -> ok - end - catch error:database_does_not_exist -> - ok - end, - UserDb. - -ensure_user_db(User) -> - UserDb = user_db_name(User), - try - {ok, _DbInfo} = fabric:get_db_info(UserDb) - catch error:database_does_not_exist -> - case fabric:create_db(UserDb, [?ADMIN_CTX]) of - ok -> ok; - accepted -> ok - end - end, - UserDb. - -add_user(User, Prop, {Modified, SecProps}) -> - {PropValue} = couch_util:get_value(Prop, SecProps, {[]}), - Names = couch_util:get_value(<<"names">>, PropValue, []), - case lists:member(User, Names) of - true -> - {Modified, SecProps}; - false -> - {true, - lists:keystore( - Prop, 1, SecProps, - {Prop, - {lists:keystore( - <<"names">>, 1, PropValue, - {<<"names">>, [User | Names]})}})} - end. - -remove_user(User, Prop, {Modified, SecProps}) -> - {PropValue} = couch_util:get_value(Prop, SecProps, {[]}), - Names = couch_util:get_value(<<"names">>, PropValue, []), - case lists:member(User, Names) of - false -> - {Modified, SecProps}; - true -> - {true, - lists:keystore( - Prop, 1, SecProps, - {Prop, - {lists:keystore( - <<"names">>, 1, PropValue, - {<<"names">>, lists:delete(User, Names)})}})} - end. - -ensure_security(User, UserDb, TransformFun) -> - {ok, Shards} = fabric:get_all_security(UserDb, [?ADMIN_CTX]), - {_ShardInfo, {SecProps}} = hd(Shards), - % assert that shards have the same security object - true = lists:all(fun ({_, {SecProps1}}) -> - SecProps =:= SecProps1 - end, Shards), - case lists:foldl( - fun (Prop, SAcc) -> TransformFun(User, Prop, SAcc) end, - {false, SecProps}, - [<<"admins">>, <<"members">>]) of - {false, _} -> - ok; - {true, SecProps1} -> - ok = fabric:set_security(UserDb, {SecProps1}, [?ADMIN_CTX]) - end. - -user_db_name(User) -> - HexUser = list_to_binary( - [string:to_lower(integer_to_list(X, 16)) || <<X>> <= User]), - <<?USERDB_PREFIX,HexUser/binary>>. - - -%% gen_server callbacks - -init([]) -> - ok = config:listen_for_changes(?MODULE, self()), - {ok, init()}. - -handle_call(_Msg, _From, State) -> - {reply, error, State}. - -handle_cast(update_config, State) when State#state.changes_pid =/= undefined -> - % we don't want to have multiple changes handler at the same time - demonitor(State#state.changes_ref, [flush]), - exit(State#state.changes_pid, kill), - {noreply, init()}; -handle_cast(update_config, _State) -> - {noreply, init()}; -handle_cast(stop, State) -> - {stop, normal, State}; -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info({'DOWN', Ref, _, _, _Reason}, #state{changes_ref=Ref} = State) -> - {stop, normal, State}; -handle_info(_Msg, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - %% Everything should be linked or monitored, let nature - %% take its course. - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - - -%% config_listener callbacks - -handle_config_change("couch_httpd_auth", "authentication_db", _Value, _Persist, Server) -> - ok = gen_server:cast(Server, update_config), - {ok, Server}; -handle_config_change("couchdb_peruser", _Key, _Value, _Persist, Server) -> - ok = gen_server:cast(Server, update_config), - {ok, Server}; -handle_config_change(_Section, _Key, _Value, _Persist, Server) -> - {ok, Server}. - -handle_config_terminate(_Self, Reason, _Server) -> - {stop, Reason}. http://git-wip-us.apache.org/repos/asf/couchdb-peruser/blob/27f4799a/test/couch_peruser_test.erl ---------------------------------------------------------------------- diff --git a/test/couch_peruser_test.erl b/test/couch_peruser_test.erl new file mode 100644 index 0000000..aac52e8 --- /dev/null +++ b/test/couch_peruser_test.erl @@ -0,0 +1,279 @@ +% Licensed under the Apache License, Version 2.0 (the "License"); you may not +% use this file except in compliance with the License. You may obtain a copy of +% the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +% License for the specific language governing permissions and limitations under +% the License. + +-module(couch_peruser_test). + +-include_lib("couch/include/couch_eunit.hrl"). +-include_lib("couch/include/couch_db.hrl"). + +-define(ADMIN_USERNAME, "admin"). +-define(ADMIN_PASSWORD, "secret"). + +setup_all() -> + TestCtx = test_util:start_couch([chttpd]), + config:set("admins", ?ADMIN_USERNAME, ?ADMIN_PASSWORD), + TestCtx. + +teardown_all(TestCtx) -> + config:delete("admins", ?ADMIN_USERNAME), + test_util:stop_couch(TestCtx). + +setup() -> + TestAuthDb = ?tempdb(), + do_request(put, get_base_url() ++ "/" ++ ?b2l(TestAuthDb)), + set_config("couch_httpd_auth", "authentication_db", ?b2l(TestAuthDb)), + set_config("couch_peruser", "enable", "true"), + TestAuthDb. + +teardown(TestAuthDb) -> + set_config("couch_httpd_auth", "authentication_db", "_users"), + set_config("couch_peruser", "enable", "false"), + set_config("couch_peruser", "delete_dbs", "false"), + do_request(delete, get_base_url() ++ "/" ++ ?b2l(TestAuthDb)), + lists:foreach(fun (DbName) -> + case DbName of + <<"userdb-",_/binary>> -> delete_db(DbName); + _ -> ok + end + end, all_dbs()). + +set_config(Section, Key, Value) -> + Url = lists:concat([ + get_base_url(), "/_config/", Section, "/", Key]), + do_request(put, Url, "\"" ++ Value ++ "\""). + +do_request(Method, Url) -> + Headers = [{basic_auth, {?ADMIN_USERNAME, ?ADMIN_PASSWORD}}], + {ok, _, _, _} = test_request:request(Method, Url, Headers). + +do_request(Method, Url, Body) -> + Headers = [ + {basic_auth, {?ADMIN_USERNAME, ?ADMIN_PASSWORD}}, + {"Content-Type", "application/json"}], + {ok, _, _, _} = test_request:request(Method, Url, Headers, Body). + +create_db(DbName) -> + {ok, _, _, _} = do_request(put, get_cluster_base_url() ++ "/" ++ ?b2l(DbName)). + +delete_db(DbName) -> + {ok, _, _, _} = do_request(delete, get_cluster_base_url() ++ "/" ++ ?b2l(DbName)). + +create_user(AuthDb, Name) -> + Body = "{\"name\":\"" ++ Name ++ + "\",\"type\":\"user\",\"roles\":[],\"password\":\"secret\"}", + Url = lists:concat([ + get_base_url(), "/", ?b2l(AuthDb), "/org.couchdb.user:", Name]), + {ok, 201, _, _} = do_request(put, Url, Body), + % let's proceed after giving couch_peruser some time to create the user db + timer:sleep(1000). + +delete_user(AuthDb, Name) -> + Url = lists:concat([get_base_url(), "/", ?b2l(AuthDb), + "/org.couchdb.user:", Name]), + {ok, 200, _, Body} = do_request(get, Url), + {DocProps} = jiffy:decode(Body), + Rev = proplists:get_value(<<"_rev">>, DocProps), + {ok, 200, _, _} = do_request(delete, Url ++ "?rev=" ++ ?b2l(Rev)), + % let's proceed after giving couch_peruser some time to delete the user db + timer:sleep(1000). + +get_security(DbName) -> + Url = lists:concat([ + get_cluster_base_url(), "/", ?b2l(DbName), "/_security"]), + {ok, 200, _, Body} = do_request(get, Url), + {SecurityProperties} = jiffy:decode(Body), + SecurityProperties. + +set_security(DbName, SecurityProperties) -> + Url = lists:concat([ + get_cluster_base_url(), "/", ?b2l(DbName), "/_security"]), + Body = jiffy:encode({SecurityProperties}), + {ok, 200, _, _} = do_request(put, Url, Body). + +all_dbs() -> + {ok, 200, _, Body} = do_request(get, get_cluster_base_url() ++ "/_all_dbs"), + jiffy:decode(Body). + +get_base_url() -> + Addr = config:get("httpd", "bind_address", "127.0.0.1"), + Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), + "http://" ++ Addr ++ ":" ++ Port. + +get_cluster_base_url() -> + Addr = config:get("httpd", "bind_address", "127.0.0.1"), + Port = integer_to_list(mochiweb_socket_server:get(chttpd, port)), + "http://" ++ Addr ++ ":" ++ Port. + +should_create_user_db(TestAuthDb) -> + create_user(TestAuthDb, "foo"), + ?_assert(lists:member(<<"userdb-666f6f">>, all_dbs())). + +should_not_delete_user_db(TestAuthDb) -> + User = "foo", + UserDbName = <<"userdb-666f6f">>, + create_user(TestAuthDb, User), + ?assert(lists:member(UserDbName, all_dbs())), + delete_user(TestAuthDb, User), + ?_assert(lists:member(UserDbName, all_dbs())). + +should_delete_user_db(TestAuthDb) -> + User = "bar", + UserDbName = <<"userdb-626172">>, + set_config("couch_peruser", "delete_dbs", "true"), + create_user(TestAuthDb, User), + ?assert(lists:member(UserDbName, all_dbs())), + delete_user(TestAuthDb, User), + ?_assert(not lists:member(UserDbName, all_dbs())). + +should_reflect_config_changes(TestAuthDb) -> + User = "baz", + UserDbName = <<"userdb-62617a">>, + set_config("couch_peruser", "delete_dbs", "true"), + create_user(TestAuthDb, User), + ?assert(lists:member(UserDbName, all_dbs())), + delete_user(TestAuthDb, User), + ?assert(not lists:member(UserDbName, all_dbs())), + create_user(TestAuthDb, User), + ?assert(lists:member(UserDbName, all_dbs())), + set_config("couch_peruser", "delete_dbs", "false"), + delete_user(TestAuthDb, User), + ?assert(lists:member(UserDbName, all_dbs())), + create_user(TestAuthDb, User), + set_config("couch_peruser", "delete_dbs", "true"), + delete_user(TestAuthDb, User), + ?assert(not lists:member(UserDbName, all_dbs())), + set_config("couch_peruser", "enable", "false"), + create_user(TestAuthDb, User), + ?_assert(not lists:member(UserDbName, all_dbs())). + +should_add_user_to_db_admins(TestAuthDb) -> + User = "qux", + UserDbName = <<"userdb-717578">>, + create_user(TestAuthDb, User), + ?_assertEqual( + {[{<<"names">>,[<<"qux">>]}]}, + proplists:get_value(<<"admins">>, get_security(UserDbName))). + +should_add_user_to_db_members(TestAuthDb) -> + User = "qux", + UserDbName = <<"userdb-717578">>, + create_user(TestAuthDb, User), + ?_assertEqual( + {[{<<"names">>,[<<"qux">>]}]}, + proplists:get_value(<<"members">>, get_security(UserDbName))). + +should_not_remove_existing_db_admins(TestAuthDb) -> + User = "qux", + UserDbName = <<"userdb-717578">>, + SecurityProperties = [ + {<<"admins">>,{[{<<"names">>,[<<"foo">>,<<"bar">>]}]}}, + {<<"members">>,{[{<<"names">>,[<<"baz">>,<<"pow">>]}]}} + ], + create_db(UserDbName), + set_security(UserDbName, SecurityProperties), + create_user(TestAuthDb, User), + {AdminProperties} = proplists:get_value(<<"admins">>, + get_security(UserDbName)), + AdminNames = proplists:get_value(<<"names">>, AdminProperties), + ?_assert(lists:member(<<"foo">>, AdminNames)), + ?_assert(lists:member(<<"bar">>, AdminNames)), + ?_assert(lists:member(<<"qux">>, AdminNames)). + +should_not_remove_existing_db_members(TestAuthDb) -> + User = "qux", + UserDbName = <<"userdb-717578">>, + SecurityProperties = [ + {<<"admins">>,{[{<<"names">>,[<<"pow">>,<<"wow">>]}]}}, + {<<"members">>,{[{<<"names">>,[<<"pow">>,<<"wow">>]}]}} + ], + create_db(UserDbName), + set_security(UserDbName, SecurityProperties), + create_user(TestAuthDb, User), + {MemberProperties} = proplists:get_value(<<"members">>, + get_security(UserDbName)), + MemberNames = proplists:get_value(<<"names">>, MemberProperties), + ?_assert(lists:member(<<"pow">>, MemberNames)), + ?_assert(lists:member(<<"wow">>, MemberNames)), + ?_assert(lists:member(<<"qux">>, MemberNames)). + +should_remove_user_from_db_admins(TestAuthDb) -> + User = "qux", + UserDbName = <<"userdb-717578">>, + SecurityProperties = [ + {<<"admins">>,{[{<<"names">>,[<<"foo">>,<<"bar">>]}]}}, + {<<"members">>,{[{<<"names">>,[<<"baz">>,<<"pow">>]}]}} + ], + create_db(UserDbName), + set_security(UserDbName, SecurityProperties), + create_user(TestAuthDb, User), + {AdminProperties} = proplists:get_value(<<"admins">>, + get_security(UserDbName)), + AdminNames = proplists:get_value(<<"names">>, AdminProperties), + ?assert(lists:member(<<"foo">>, AdminNames)), + ?assert(lists:member(<<"bar">>, AdminNames)), + ?assert(lists:member(<<"qux">>, AdminNames)), + delete_user(TestAuthDb, User), + {NewAdminProperties} = proplists:get_value(<<"admins">>, + get_security(UserDbName)), + NewAdminNames = proplists:get_value(<<"names">>, NewAdminProperties), + ?_assert(lists:member(<<"foo">>, NewAdminNames)), + ?_assert(lists:member(<<"bar">>, NewAdminNames)), + ?_assert(not lists:member(<<"qux">>, NewAdminNames)). + +should_remove_user_from_db_members(TestAuthDb) -> + User = "qux", + UserDbName = <<"userdb-717578">>, + SecurityProperties = [ + {<<"admins">>,{[{<<"names">>,[<<"pow">>,<<"wow">>]}]}}, + {<<"members">>,{[{<<"names">>,[<<"pow">>,<<"wow">>]}]}} + ], + create_db(UserDbName), + set_security(UserDbName, SecurityProperties), + create_user(TestAuthDb, User), + {MemberProperties} = proplists:get_value(<<"members">>, + get_security(UserDbName)), + MemberNames = proplists:get_value(<<"names">>, MemberProperties), + ?assert(lists:member(<<"pow">>, MemberNames)), + ?assert(lists:member(<<"wow">>, MemberNames)), + ?assert(lists:member(<<"qux">>, MemberNames)), + delete_user(TestAuthDb, User), + {NewMemberProperties} = proplists:get_value(<<"members">>, + get_security(UserDbName)), + NewMemberNames = proplists:get_value(<<"names">>, NewMemberProperties), + ?_assert(lists:member(<<"foo">>, NewMemberNames)), + ?_assert(lists:member(<<"bar">>, NewMemberNames)), + ?_assert(not lists:member(<<"qux">>, NewMemberNames)). + +couch_peruser_test_() -> + { + "couch_peruser test", + { + setup, + fun setup_all/0, fun teardown_all/1, + { + foreach, + fun setup/0, fun teardown/1, + [ + fun should_create_user_db/1, + fun should_not_delete_user_db/1, + fun should_delete_user_db/1, + fun should_reflect_config_changes/1, + fun should_add_user_to_db_admins/1, + fun should_add_user_to_db_members/1, + fun should_not_remove_existing_db_admins/1, + fun should_not_remove_existing_db_members/1, + fun should_remove_user_from_db_admins/1, + fun should_remove_user_from_db_members/1 + ] + } + } + }. http://git-wip-us.apache.org/repos/asf/couchdb-peruser/blob/27f4799a/test/couchdb_peruser_test.erl ---------------------------------------------------------------------- diff --git a/test/couchdb_peruser_test.erl b/test/couchdb_peruser_test.erl deleted file mode 100644 index 3913d64..0000000 --- a/test/couchdb_peruser_test.erl +++ /dev/null @@ -1,279 +0,0 @@ -% Licensed under the Apache License, Version 2.0 (the "License"); you may not -% use this file except in compliance with the License. You may obtain a copy of -% the License at -% -% http://www.apache.org/licenses/LICENSE-2.0 -% -% Unless required by applicable law or agreed to in writing, software -% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -% License for the specific language governing permissions and limitations under -% the License. - --module(couchdb_peruser_test). - --include_lib("couch/include/couch_eunit.hrl"). --include_lib("couch/include/couch_db.hrl"). - --define(ADMIN_USERNAME, "admin"). --define(ADMIN_PASSWORD, "secret"). - -setup_all() -> - TestCtx = test_util:start_couch([chttpd]), - config:set("admins", ?ADMIN_USERNAME, ?ADMIN_PASSWORD), - TestCtx. - -teardown_all(TestCtx) -> - config:delete("admins", ?ADMIN_USERNAME), - test_util:stop_couch(TestCtx). - -setup() -> - TestAuthDb = ?tempdb(), - do_request(put, get_base_url() ++ "/" ++ ?b2l(TestAuthDb)), - set_config("couch_httpd_auth", "authentication_db", ?b2l(TestAuthDb)), - set_config("couchdb_peruser", "enable", "true"), - TestAuthDb. - -teardown(TestAuthDb) -> - set_config("couch_httpd_auth", "authentication_db", "_users"), - set_config("couchdb_peruser", "enable", "false"), - set_config("couchdb_peruser", "delete_dbs", "false"), - do_request(delete, get_base_url() ++ "/" ++ ?b2l(TestAuthDb)), - lists:foreach(fun (DbName) -> - case DbName of - <<"userdb-",_/binary>> -> delete_db(DbName); - _ -> ok - end - end, all_dbs()). - -set_config(Section, Key, Value) -> - Url = lists:concat([ - get_base_url(), "/_config/", Section, "/", Key]), - do_request(put, Url, "\"" ++ Value ++ "\""). - -do_request(Method, Url) -> - Headers = [{basic_auth, {?ADMIN_USERNAME, ?ADMIN_PASSWORD}}], - {ok, _, _, _} = test_request:request(Method, Url, Headers). - -do_request(Method, Url, Body) -> - Headers = [ - {basic_auth, {?ADMIN_USERNAME, ?ADMIN_PASSWORD}}, - {"Content-Type", "application/json"}], - {ok, _, _, _} = test_request:request(Method, Url, Headers, Body). - -create_db(DbName) -> - {ok, _, _, _} = do_request(put, get_cluster_base_url() ++ "/" ++ ?b2l(DbName)). - -delete_db(DbName) -> - {ok, _, _, _} = do_request(delete, get_cluster_base_url() ++ "/" ++ ?b2l(DbName)). - -create_user(AuthDb, Name) -> - Body = "{\"name\":\"" ++ Name ++ - "\",\"type\":\"user\",\"roles\":[],\"password\":\"secret\"}", - Url = lists:concat([ - get_base_url(), "/", ?b2l(AuthDb), "/org.couchdb.user:", Name]), - {ok, 201, _, _} = do_request(put, Url, Body), - % let's proceed after giving couchdb_peruser some time to create the user db - timer:sleep(1000). - -delete_user(AuthDb, Name) -> - Url = lists:concat([get_base_url(), "/", ?b2l(AuthDb), - "/org.couchdb.user:", Name]), - {ok, 200, _, Body} = do_request(get, Url), - {DocProps} = jiffy:decode(Body), - Rev = proplists:get_value(<<"_rev">>, DocProps), - {ok, 200, _, _} = do_request(delete, Url ++ "?rev=" ++ ?b2l(Rev)), - % let's proceed after giving couchdb_peruser some time to delete the user db - timer:sleep(1000). - -get_security(DbName) -> - Url = lists:concat([ - get_cluster_base_url(), "/", ?b2l(DbName), "/_security"]), - {ok, 200, _, Body} = do_request(get, Url), - {SecurityProperties} = jiffy:decode(Body), - SecurityProperties. - -set_security(DbName, SecurityProperties) -> - Url = lists:concat([ - get_cluster_base_url(), "/", ?b2l(DbName), "/_security"]), - Body = jiffy:encode({SecurityProperties}), - {ok, 200, _, _} = do_request(put, Url, Body). - -all_dbs() -> - {ok, 200, _, Body} = do_request(get, get_cluster_base_url() ++ "/_all_dbs"), - jiffy:decode(Body). - -get_base_url() -> - Addr = config:get("httpd", "bind_address", "127.0.0.1"), - Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), - "http://" ++ Addr ++ ":" ++ Port. - -get_cluster_base_url() -> - Addr = config:get("httpd", "bind_address", "127.0.0.1"), - Port = integer_to_list(mochiweb_socket_server:get(chttpd, port)), - "http://" ++ Addr ++ ":" ++ Port. - -should_create_user_db(TestAuthDb) -> - create_user(TestAuthDb, "foo"), - ?_assert(lists:member(<<"userdb-666f6f">>, all_dbs())). - -should_not_delete_user_db(TestAuthDb) -> - User = "foo", - UserDbName = <<"userdb-666f6f">>, - create_user(TestAuthDb, User), - ?assert(lists:member(UserDbName, all_dbs())), - delete_user(TestAuthDb, User), - ?_assert(lists:member(UserDbName, all_dbs())). - -should_delete_user_db(TestAuthDb) -> - User = "bar", - UserDbName = <<"userdb-626172">>, - set_config("couchdb_peruser", "delete_dbs", "true"), - create_user(TestAuthDb, User), - ?assert(lists:member(UserDbName, all_dbs())), - delete_user(TestAuthDb, User), - ?_assert(not lists:member(UserDbName, all_dbs())). - -should_reflect_config_changes(TestAuthDb) -> - User = "baz", - UserDbName = <<"userdb-62617a">>, - set_config("couchdb_peruser", "delete_dbs", "true"), - create_user(TestAuthDb, User), - ?assert(lists:member(UserDbName, all_dbs())), - delete_user(TestAuthDb, User), - ?assert(not lists:member(UserDbName, all_dbs())), - create_user(TestAuthDb, User), - ?assert(lists:member(UserDbName, all_dbs())), - set_config("couchdb_peruser", "delete_dbs", "false"), - delete_user(TestAuthDb, User), - ?assert(lists:member(UserDbName, all_dbs())), - create_user(TestAuthDb, User), - set_config("couchdb_peruser", "delete_dbs", "true"), - delete_user(TestAuthDb, User), - ?assert(not lists:member(UserDbName, all_dbs())), - set_config("couchdb_peruser", "enable", "false"), - create_user(TestAuthDb, User), - ?_assert(not lists:member(UserDbName, all_dbs())). - -should_add_user_to_db_admins(TestAuthDb) -> - User = "qux", - UserDbName = <<"userdb-717578">>, - create_user(TestAuthDb, User), - ?_assertEqual( - {[{<<"names">>,[<<"qux">>]}]}, - proplists:get_value(<<"admins">>, get_security(UserDbName))). - -should_add_user_to_db_members(TestAuthDb) -> - User = "qux", - UserDbName = <<"userdb-717578">>, - create_user(TestAuthDb, User), - ?_assertEqual( - {[{<<"names">>,[<<"qux">>]}]}, - proplists:get_value(<<"members">>, get_security(UserDbName))). - -should_not_remove_existing_db_admins(TestAuthDb) -> - User = "qux", - UserDbName = <<"userdb-717578">>, - SecurityProperties = [ - {<<"admins">>,{[{<<"names">>,[<<"foo">>,<<"bar">>]}]}}, - {<<"members">>,{[{<<"names">>,[<<"baz">>,<<"pow">>]}]}} - ], - create_db(UserDbName), - set_security(UserDbName, SecurityProperties), - create_user(TestAuthDb, User), - {AdminProperties} = proplists:get_value(<<"admins">>, - get_security(UserDbName)), - AdminNames = proplists:get_value(<<"names">>, AdminProperties), - ?_assert(lists:member(<<"foo">>, AdminNames)), - ?_assert(lists:member(<<"bar">>, AdminNames)), - ?_assert(lists:member(<<"qux">>, AdminNames)). - -should_not_remove_existing_db_members(TestAuthDb) -> - User = "qux", - UserDbName = <<"userdb-717578">>, - SecurityProperties = [ - {<<"admins">>,{[{<<"names">>,[<<"pow">>,<<"wow">>]}]}}, - {<<"members">>,{[{<<"names">>,[<<"pow">>,<<"wow">>]}]}} - ], - create_db(UserDbName), - set_security(UserDbName, SecurityProperties), - create_user(TestAuthDb, User), - {MemberProperties} = proplists:get_value(<<"members">>, - get_security(UserDbName)), - MemberNames = proplists:get_value(<<"names">>, MemberProperties), - ?_assert(lists:member(<<"pow">>, MemberNames)), - ?_assert(lists:member(<<"wow">>, MemberNames)), - ?_assert(lists:member(<<"qux">>, MemberNames)). - -should_remove_user_from_db_admins(TestAuthDb) -> - User = "qux", - UserDbName = <<"userdb-717578">>, - SecurityProperties = [ - {<<"admins">>,{[{<<"names">>,[<<"foo">>,<<"bar">>]}]}}, - {<<"members">>,{[{<<"names">>,[<<"baz">>,<<"pow">>]}]}} - ], - create_db(UserDbName), - set_security(UserDbName, SecurityProperties), - create_user(TestAuthDb, User), - {AdminProperties} = proplists:get_value(<<"admins">>, - get_security(UserDbName)), - AdminNames = proplists:get_value(<<"names">>, AdminProperties), - ?assert(lists:member(<<"foo">>, AdminNames)), - ?assert(lists:member(<<"bar">>, AdminNames)), - ?assert(lists:member(<<"qux">>, AdminNames)), - delete_user(TestAuthDb, User), - {NewAdminProperties} = proplists:get_value(<<"admins">>, - get_security(UserDbName)), - NewAdminNames = proplists:get_value(<<"names">>, NewAdminProperties), - ?_assert(lists:member(<<"foo">>, NewAdminNames)), - ?_assert(lists:member(<<"bar">>, NewAdminNames)), - ?_assert(not lists:member(<<"qux">>, NewAdminNames)). - -should_remove_user_from_db_members(TestAuthDb) -> - User = "qux", - UserDbName = <<"userdb-717578">>, - SecurityProperties = [ - {<<"admins">>,{[{<<"names">>,[<<"pow">>,<<"wow">>]}]}}, - {<<"members">>,{[{<<"names">>,[<<"pow">>,<<"wow">>]}]}} - ], - create_db(UserDbName), - set_security(UserDbName, SecurityProperties), - create_user(TestAuthDb, User), - {MemberProperties} = proplists:get_value(<<"members">>, - get_security(UserDbName)), - MemberNames = proplists:get_value(<<"names">>, MemberProperties), - ?assert(lists:member(<<"pow">>, MemberNames)), - ?assert(lists:member(<<"wow">>, MemberNames)), - ?assert(lists:member(<<"qux">>, MemberNames)), - delete_user(TestAuthDb, User), - {NewMemberProperties} = proplists:get_value(<<"members">>, - get_security(UserDbName)), - NewMemberNames = proplists:get_value(<<"names">>, NewMemberProperties), - ?_assert(lists:member(<<"foo">>, NewMemberNames)), - ?_assert(lists:member(<<"bar">>, NewMemberNames)), - ?_assert(not lists:member(<<"qux">>, NewMemberNames)). - -couchdb_peruser_test_() -> - { - "couchdb_peruser test", - { - setup, - fun setup_all/0, fun teardown_all/1, - { - foreach, - fun setup/0, fun teardown/1, - [ - fun should_create_user_db/1, - fun should_not_delete_user_db/1, - fun should_delete_user_db/1, - fun should_reflect_config_changes/1, - fun should_add_user_to_db_admins/1, - fun should_add_user_to_db_members/1, - fun should_not_remove_existing_db_admins/1, - fun should_not_remove_existing_db_members/1, - fun should_remove_user_from_db_admins/1, - fun should_remove_user_from_db_members/1 - ] - } - } - }.
