You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@couchdb.apache.org by GitBox <gi...@apache.org> on 2018/03/05 20:50:57 UTC

[GitHub] wohali closed pull request #366: Allow server admins to retrieve an auth token for any other user

wohali closed pull request #366: Allow server admins to retrieve an auth token for any other user
URL: https://github.com/apache/couchdb/pull/366
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/etc/couchdb/default.ini.tpl.in b/etc/couchdb/default.ini.tpl.in
index 0c4be868b4..7f7feb1c4b 100644
--- a/etc/couchdb/default.ini.tpl.in
+++ b/etc/couchdb/default.ini.tpl.in
@@ -164,6 +164,7 @@ _restart = {couch_httpd_misc_handlers, handle_restart_req}
 _stats = {couch_httpd_stats_handlers, handle_stats_req}
 _log = {couch_httpd_misc_handlers, handle_log_req}
 _session = {couch_httpd_auth, handle_session_req}
+_login_as = {couch_httpd_auth, handle_delegated_session_req}
 _oauth = {couch_httpd_oauth, handle_oauth_req}
 _db_updates = {couch_dbupdates_httpd, handle_req}
 _plugins = {couch_plugins_httpd, handle_req}
diff --git a/src/couchdb/couch_httpd_auth.erl b/src/couchdb/couch_httpd_auth.erl
index 3052832556..afcfad7b30 100644
--- a/src/couchdb/couch_httpd_auth.erl
+++ b/src/couchdb/couch_httpd_auth.erl
@@ -18,7 +18,7 @@
 -export([null_authentication_handler/1]).
 -export([proxy_authentication_handler/1, proxy_authentification_handler/1]).
 -export([cookie_auth_header/2]).
--export([handle_session_req/1]).
+-export([handle_session_req/1,handle_delegated_session_req/1]).
 
 -import(couch_httpd, [header_value/2, send_json/2,send_json/4, send_method_not_allowed/2]).
 
@@ -232,12 +232,15 @@ cookie_auth_header(#httpd{user_ctx=#user_ctx{name=User}, auth={Secret, true}}=Re
 cookie_auth_header(_Req, _Headers) -> [].
 
 cookie_auth_cookie(Req, User, Secret, TimeStamp) ->
-    SessionData = User ++ ":" ++ erlang:integer_to_list(TimeStamp, 16),
-    Hash = crypto:sha_mac(Secret, SessionData),
     mochiweb_cookies:cookie("AuthSession",
-        couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)),
+        make_cookie_hash(User, Secret, TimeStamp),
         [{path, "/"}] ++ cookie_scheme(Req) ++ max_age()).
 
+make_cookie_hash(UserName, Secret, TimeStamp) ->
+    SessionData = UserName ++ ":" ++ erlang:integer_to_list(TimeStamp, 16),
+    Hash = crypto:sha_mac(Secret, SessionData),
+    couch_util:encodeBase64Url(SessionData ++ ":" ++ ?b2l(Hash)).
+
 ensure_cookie_auth_secret() ->
     case couch_config:get("couch_httpd_auth", "secret", nil) of
         nil ->
@@ -247,6 +250,36 @@ ensure_cookie_auth_secret() ->
         Secret -> Secret
     end.
 
+prepare_cookie_values(UserName, Password, UserProps) ->
+    UserProps2 = maybe_upgrade_password_hash(UserName, Password, UserProps),
+    % setup the session cookie
+    Secret = ?l2b(ensure_cookie_auth_secret()),
+    UserSalt = couch_util:get_value(<<"salt">>, UserProps2),
+    CurrentTime = make_cookie_time(),
+    {Secret, UserSalt, CurrentTime}.
+
+
+% This endpoint exists to allow users with a server admin account to get an
+% authentication token for any other user. This is useful when a middleware
+% layer has access to CouchDB as an admin user and needs to give browsers an
+% access token that authenticates them against CouchDB.
+handle_delegated_session_req(#httpd{method='POST', path_parts=[_LoginAs, UserName]}=Req) ->
+    ok = couch_httpd:verify_is_server_admin(Req),
+    couch_httpd:validate_ctype(Req, "application/json"),
+    case couch_auth_cache:get_user_creds(UserName) of
+        nil -> couch_httpd:send_error(Req, not_found); % maybe send better error message
+        UserProps ->
+            Password = ?l2b(couch_util:get_value("password", UserProps, "")),
+            {Secret, UserSalt, CurrentTime} = prepare_cookie_values(UserName, Password, UserProps),
+            Cookie = make_cookie_hash(?b2l(UserName), <<Secret/binary, UserSalt/binary>>, CurrentTime),
+            % Cookie = make_cookie_token(Req, UserName, UserProps),
+            send_json(Req, {[{<<"auth_token">>, Cookie}]}) % handle Cookie value string foo
+    end;
+handle_delegated_session_req(#httpd{method='POST', path_parts=[_LoginAs]}=Req) ->
+    send_json(Req, 404, [], {[{<<"error">>, <<"missing username: /_login_as/username">>}]});
+handle_delegated_session_req(Req) ->
+    send_method_not_allowed(Req, "POST").
+
 % session handlers
 % Login handler with user db
 handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
@@ -272,11 +305,7 @@ handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
     end,
     case authenticate(Password, UserProps) of
         true ->
-            UserProps2 = maybe_upgrade_password_hash(UserName, Password, UserProps),
-            % setup the session cookie
-            Secret = ?l2b(ensure_cookie_auth_secret()),
-            UserSalt = couch_util:get_value(<<"salt">>, UserProps2),
-            CurrentTime = make_cookie_time(),
+            {Secret, UserSalt, CurrentTime} = prepare_cookie_values(UserName, Password, UserProps),
             Cookie = cookie_auth_cookie(Req, ?b2l(UserName), <<Secret/binary, UserSalt/binary>>, CurrentTime),
             % TODO document the "next" feature in Futon
             {Code, Headers} = case couch_httpd:qs_value(Req, "next", nil) of
@@ -288,8 +317,8 @@ handle_session_req(#httpd{method='POST', mochi_req=MochiReq}=Req) ->
             send_json(Req#httpd{req_body=ReqBody}, Code, Headers,
                 {[
                     {ok, true},
-                    {name, couch_util:get_value(<<"name">>, UserProps2, null)},
-                    {roles, couch_util:get_value(<<"roles">>, UserProps2, [])}
+                    {name, couch_util:get_value(<<"name">>, UserProps, null)},
+                    {roles, couch_util:get_value(<<"roles">>, UserProps, [])}
                 ]});
         _Else ->
             % clear the session
diff --git a/test/etap/240-login-as.t b/test/etap/240-login-as.t
new file mode 100644
index 0000000000..cb2c6208ab
--- /dev/null
+++ b/test/etap/240-login-as.t
@@ -0,0 +1,122 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+-record(user_ctx, {
+    name = null,
+    roles = [],
+    handler
+}).
+
+server() ->
+    lists:concat([
+        "http://127.0.0.1:", mochiweb_socket_server:get(couch_httpd, port), "/"
+    ]).
+
+admin_server() ->
+    lists:concat([
+        "http://", admin_user(), ":", admin_pass(), "@127.0.0.1:", mochiweb_socket_server:get(couch_httpd, port), "/"
+    ]).
+
+
+dbname() -> "etap-test-db".
+admin_user() -> "admin".
+admin_pass() -> "asd".
+
+user() -> "foo".
+pass() -> "bar".
+
+admin_user_ctx() -> {user_ctx, #user_ctx{roles=[<<"_admin">>]}}.
+
+main(_) ->
+    test_util:init_code_path(),
+
+    etap:plan(4),
+    case (catch test()) of
+        ok ->
+            etap:end_tests();
+        Other ->
+            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+            etap:bail(Other)
+    end,
+    ok.
+
+test() ->
+    couch_server_sup:start_link(test_util:config_files()),
+    ibrowse:start(),
+    crypto:start(),
+
+    timer:sleep(1000),
+    couch_server:delete(list_to_binary(dbname()), [admin_user_ctx()]),
+    {ok, Db} = couch_db:create(list_to_binary(dbname()), [admin_user_ctx()]),
+
+    test_admin_party(),
+
+    create_admin_user(),
+    create_user(),
+
+    test_non_existent_user(),
+    test_existing_user(),
+
+    %% restart boilerplate
+    couch_db:close(Db),
+    ok = couch_server:delete(couch_db:name(Db), [admin_user_ctx()]),
+    ok = couch_server:delete("_users", [admin_user_ctx()]),
+    timer:sleep(3000),
+    couch_server_sup:stop(),
+
+    ok.
+
+create_admin_user() ->
+    ok = couch_config:set("admins", admin_user(), admin_pass(), false).
+
+create_user() ->
+    UserDoc = {[
+        {<<"_id">>, list_to_binary("org.couchdb.user:" ++ user())},
+        {<<"type">>, <<"user">>},
+        {<<"name">>, list_to_binary(user())},
+        {<<"password">>, list_to_binary(pass())},
+        {<<"roles">>, []}
+    ]},
+    case ibrowse:send_req(server() ++ "_users/org.couchdb.user:" ++ user(), [{"Content-Type", "application/json"}], put, ejson:encode(UserDoc)) of
+        {ok, _, _, _} -> ok;
+        _Else ->
+            etap:bail("http PUT /_users/org.couchdb.user:" ++ user() ++ " request failed")
+    end.
+
+test_non_existent_user() ->
+    case ibrowse:send_req(admin_server() ++ "_login_as/baz", [], post, []) of
+        {ok, Code, _Headers, _Body} ->
+            etap:is(Code, "404", "User not found");
+        _Else ->
+            etap:bail("http POST /_login_as/bar request failed")
+    end.
+
+test_existing_user() ->
+    case ibrowse:send_req(admin_server() ++ "_login_as/foo", [], post, []) of
+        {ok, Code, _Headers, Body} ->
+            etap:is(Code, "200", "User found"),
+            {[{Key, _}]} = ejson:decode(Body),
+            etap:is(Key, <<"auth_token">>, "has auth_token");
+        _Else ->
+            etap:bail("http POST /_login_as/foo request failed")
+    end.
+
+test_admin_party() ->
+    case ibrowse:send_req(server() ++ "_login_as/foo", [], post, []) of
+        {ok, Code, _Headers, _Body} ->
+            etap:is(Code, "404", "User not found");
+        _Else ->
+            etap:bail("http POST /_login_as request failed")
+    end.
diff --git a/test/etap/Makefile.am b/test/etap/Makefile.am
index c9778ca6ec..33c3b3115e 100644
--- a/test/etap/Makefile.am
+++ b/test/etap/Makefile.am
@@ -100,6 +100,7 @@ tap_files = \
     220-compaction-daemon.t \
     230-pbkfd2.t \
     231-cors.t \
+    240-login-as.t \
     250-upgrade-legacy-view-files.t
 
 EXTRA_DIST = \


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services