]> granicus.if.org Git - ejabberd/commitdiff
Add 'ejabberd:user' and 'ejabberd:admin' oauth scopes
authorPablo Polvorin <ppolvorin@process-one.net>
Tue, 19 Jul 2016 02:27:49 +0000 (23:27 -0300)
committerPablo Polvorin <ppolvorin@process-one.net>
Tue, 19 Jul 2016 03:24:06 +0000 (00:24 -0300)
'ejabberd:user' includes all commands defined with policy "user".
'ejabberd:admin' includes commands defined with policy "admin".

include/ejabberd_commands.hrl
src/ejabberd_commands.erl
src/ejabberd_oauth.erl
src/mod_http_api.erl
test/mod_http_api_mock_test.exs

index 81be06dc36e4d0f6b38200959a792bc881834cbd..2b4eca581b6d0af5c9a6f09cf103977d983916bc 100644 (file)
@@ -26,6 +26,8 @@
                  {tuple, [rterm()]} | {list, rterm()} |
                  rescode | restuple.
 
+-type oauth_scope() :: atom().
+
 -record(ejabberd_commands,
        {name                    :: atom(),
          tags = []               :: [atom()] | '_' | '$2',
index 9d41f50c2230be5bda402c5d63d3e9a132777c06..075ff35cfc267dfe86647acc0aa852edb1752d8b 100644 (file)
         get_command_format/1,
         get_command_format/2,
         get_command_format/3,
-         get_command_policy/1,
+         get_command_policy_and_scope/1,
         get_command_definition/1,
         get_command_definition/2,
         get_tags_commands/0,
@@ -366,17 +366,23 @@ get_command_format(Name, Auth, Version) ->
            {Args, Result}
     end.
 
--spec get_command_policy(atom()) -> {ok, open|user|admin|restricted} | {error, command_not_found}.
+-spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}.
 
 %% @doc return command policy.
-get_command_policy(Name) ->
+get_command_policy_and_scope(Name) ->
     case get_command_definition(Name) of
-        #ejabberd_commands{policy = Policy} ->
-            {ok, Policy};
+        #ejabberd_commands{policy = Policy} = Cmd ->
+            {ok, Policy, cmd_scope(Cmd)};
         command_not_found ->
             {error, command_not_found}
     end.
 
+%% The oauth scopes for a command are the command name itself,
+%% also might include either 'ejabberd:user' or 'ejabberd:admin'
+cmd_scope(#ejabberd_commands{policy = Policy, name = Name}) ->
+    [erlang:atom_to_binary(Name,utf8)] ++ [<<"ejabberd:user">> || Policy == user] ++ [<<"ejabberd:admin">> || Policy == admin].
+
+
 -spec get_command_definition(atom()) -> ejabberd_commands().
 
 %% @doc Get the definition record of a command.
@@ -627,8 +633,8 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI
 check_auth(_Command, noauth) ->
     no_auth_provided;
 check_auth(Command, {User, Server, {oauth, Token}, _}) ->
-    Scope = erlang:atom_to_binary(Command#ejabberd_commands.name, utf8),
-    case ejabberd_oauth:check_token(User, Server, Scope, Token) of
+    ScopeList = cmd_scope(Command),
+    case ejabberd_oauth:check_token(User, Server, ScopeList, Token) of
         true ->
             {ok, User, Server};
         false ->
index 246bac127cbabf57fc48c7d725704f221044ffc9..0397571b281aa0fd4044a74857f7fe8f8bc69b76 100644 (file)
@@ -90,7 +90,7 @@ start() ->
 get_commands_spec() ->
     [
      #ejabberd_commands{name = oauth_issue_token, tags = [oauth],
-                        desc = "Issue an oauth token. Available scopes are the ones usable by ejabberd admins",
+                        desc = "Issue an oauth token for the given jid",
                         module = ?MODULE, function = oauth_issue_token,
                         args = [{jid, string},{scopes, string}],
                         policy = restricted,
@@ -106,11 +106,11 @@ get_commands_spec() ->
                         result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}
                        },
      #ejabberd_commands{name = oauth_list_scopes, tags = [oauth],
-                        desc = "List scopes that can be granted to tokens generated through the command line",
+                        desc = "List scopes that can be granted to tokens generated through the command line, together with the commands they allow",
                         module = ?MODULE, function = oauth_list_scopes,
                         args = [],
                         policy = restricted,
-                        result = {scopes, {list, {scope, string}}}
+                        result = {scopes, {list, {scope, {tuple, [{scope, string}, {commands, string}]}}}}
                        },
      #ejabberd_commands{name = oauth_revoke_token, tags = [oauth],
                         desc = "Revoke authorization for a token",
@@ -153,7 +153,7 @@ oauth_revoke_token(Token) ->
     oauth_list_tokens().
 
 oauth_list_scopes() ->
-    get_cmd_scopes().
+    [ {Scope, string:join([atom_to_list(Cmd) || Cmd <- Cmds], ",")}   || {Scope, Cmds} <- dict:to_list(get_cmd_scopes())].
 
 
 
@@ -240,7 +240,7 @@ authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}.
 
 verify_resowner_scope({user, _User, _Server}, Scope, Ctx) ->
     Cmds = ejabberd_commands:get_commands(),
-    Cmds1 = [sasl_auth | Cmds],
+    Cmds1 = ['ejabberd:user', 'ejabberd:admin', sasl_auth | Cmds],
     RegisteredScope = [atom_to_binary(C, utf8) || C <- Cmds1],
     case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
                                    oauth2_priv_set:new(RegisteredScope)) of
@@ -254,17 +254,27 @@ verify_resowner_scope(_, _, _) ->
 
 
 get_cmd_scopes() ->
-    Cmds = lists:filter(fun(Cmd) -> case ejabberd_commands:get_command_policy(Cmd) of
-                                        {ok, Policy} when Policy =/= restricted -> true;
-                                        _ -> false
-                                    end end,
-                        ejabberd_commands:get_commands()),
-    [atom_to_binary(C, utf8) || C <- Cmds].
+    ScopeMap = lists:foldl(fun(Cmd, Accum) ->
+                        case ejabberd_commands:get_command_policy_and_scope(Cmd) of
+                            {ok, Policy, Scopes} when Policy =/= restricted ->
+                                lists:foldl(fun(Scope, Accum2) ->
+                                                    dict:append(Scope, Cmd, Accum2)
+                                            end, Accum, Scopes);
+                            _ -> Accum
+                        end end, dict:new(), ejabberd_commands:get_commands()),
+    ScopeMap.
+
+    %Scps = lists:flatmap(fun(Cmd) -> case ejabberd_commands:get_command_policy_and_scope(Cmd) of
+    %                                    {ok, Policy, Scopes} when Policy =/= restricted -> Scopes;
+    %                                    _ -> []
+    %                                end end,
+    %                    ejabberd_commands:get_commands()),
+    %lists:usort(Scps).
 
 %% This is callback for oauth tokens generated through the command line.  Only open and admin commands are
 %% made available.
 verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
-    RegisteredScope = get_cmd_scopes(),
+    RegisteredScope = dict:fetch_keys(get_cmd_scopes()),
     case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
                                    oauth2_priv_set:new(RegisteredScope)) of
         true ->
@@ -299,7 +309,7 @@ associate_refresh_token(_RefreshToken, _Context, AppContext) ->
     {ok, AppContext}.
 
 
-check_token(User, Server, Scope, Token) ->
+check_token(User, Server, ScopeList, Token) ->
     LUser = jid:nodeprep(User),
     LServer = jid:nameprep(Server),
     case catch mnesia:dirty_read(oauth_token, Token) of
@@ -308,23 +318,25 @@ check_token(User, Server, Scope, Token) ->
                       expire = Expire}] ->
             {MegaSecs, Secs, _} = os:timestamp(),
             TS = 1000000 * MegaSecs + Secs,
-            oauth2_priv_set:is_member(
-              Scope, oauth2_priv_set:new(TokenScope)) andalso
-                Expire > TS;
+            TokenScopeSet = oauth2_priv_set:new(TokenScope),
+            lists:any(fun(Scope) ->
+                oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
+                ScopeList) andalso Expire > TS;
         _ ->
             false
     end.
 
-check_token(Scope, Token) ->
+check_token(ScopeList, Token) ->
     case catch mnesia:dirty_read(oauth_token, Token) of
         [#oauth_token{us = US,
                       scope = TokenScope,
                       expire = Expire}] ->
             {MegaSecs, Secs, _} = os:timestamp(),
             TS = 1000000 * MegaSecs + Secs,
-            case oauth2_priv_set:is_member(
-                   Scope, oauth2_priv_set:new(TokenScope)) andalso
-                Expire > TS of
+            TokenScopeSet = oauth2_priv_set:new(TokenScope),
+            case lists:any(fun(Scope) ->
+                oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
+                ScopeList) andalso Expire > TS of
                 true -> {ok, user, US};
                 false -> false
             end;
index 1b4aa502ba6cd6fdc610384653b32d98c99caf69..f6621c09f225d63cf2707c8c14b0bf910c8f39c0 100644 (file)
@@ -133,13 +133,13 @@ depends(_Host, _Opts) ->
 check_permissions(Request, Command) ->
     case catch binary_to_existing_atom(Command, utf8) of
         Call when is_atom(Call) ->
-            {ok, CommandPolicy} = ejabberd_commands:get_command_policy(Call),
-            check_permissions2(Request, Call, CommandPolicy);
+            {ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call),
+            check_permissions2(Request, Call, CommandPolicy, Scope);
         _ ->
             unauthorized_response()
     end.
 
-check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
+check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList)
   when HTTPAuth /= undefined ->
     Admin =
         case lists:keysearch(<<"X-Admin">>, 1, Headers) of
@@ -159,7 +159,7 @@ check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
                         false
                 end;
             {oauth, Token, _} ->
-                case oauth_check_token(Call, Token) of
+                case oauth_check_token(ScopeList, Token) of
                     {ok, user, {User, Server}} ->
                         {ok, {User, Server, {oauth, Token}, Admin}};
                     false ->
@@ -172,9 +172,9 @@ check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
         {ok, A} -> {allowed, Call, A};
         _ -> unauthorized_response()
     end;
-check_permissions2(_Request, Call, open) ->
+check_permissions2(_Request, Call, open, _Scope) ->
     {allowed, Call, noauth};
-check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) ->
+check_permissions2(#request{ip={IP, _Port}}, Call, _Policy, _Scope) ->
     Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
                                     fun(V) -> V end,
                                     none),
@@ -194,13 +194,11 @@ check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) ->
         _E ->
             {allowed, Call, noauth}
     end;
-check_permissions2(_Request, _Call, _Policy) ->
+check_permissions2(_Request, _Call, _Policy, _Scope) ->
     unauthorized_response().
 
-oauth_check_token(Scope, Token) when is_atom(Scope) ->
-    oauth_check_token(atom_to_binary(Scope, utf8), Token);
-oauth_check_token(Scope, Token) ->
-    ejabberd_oauth:check_token(Scope, Token).
+oauth_check_token(ScopeList, Token) when is_list(ScopeList) ->
+    ejabberd_oauth:check_token(ScopeList, Token).
 
 %% ------------------
 %% command processing
index 47b1fe94ae130bbe8d6eddd4853c6c7434766b82..db8761887f1517ede51ea6b20deb6d35516bb97a 100644 (file)
@@ -70,8 +70,8 @@ defmodule ModHttpApiMockTest do
                        fn (@acommand, {@user, @domain, @userpass, false}, @version) ->
                                {[], {:res, :rescode}}
                        end)
-    :meck.expect(:ejabberd_commands, :get_command_policy,
-                       fn (@acommand) -> {:ok, :user} end)
+    :meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
+                       fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8)]} end)
                :meck.expect(:ejabberd_commands, :get_commands,
                        fn () -> [@acommand] end)
                :meck.expect(:ejabberd_commands, :execute_command,
@@ -123,8 +123,8 @@ defmodule ModHttpApiMockTest do
                        fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) ->
                                        {[], {:res, :rescode}}
                        end)
-    :meck.expect(:ejabberd_commands, :get_command_policy,
-                       fn (@acommand) -> {:ok, :user} end)
+    :meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
+                       fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end)
                :meck.expect(:ejabberd_commands, :get_commands,
                        fn () -> [@acommand] end)
                :meck.expect(:ejabberd_commands, :execute_command,
@@ -134,7 +134,7 @@ defmodule ModHttpApiMockTest do
                        end)
 
 
-               # Correct OAuth call
+               # Correct OAuth call using specific scope
                token = EjabberdOauthMock.get_token @user, @domain, @command
                req = request(method: :GET,
                                                                        path: ["api", @command],
@@ -147,6 +147,19 @@ defmodule ModHttpApiMockTest do
                assert 200 == elem(result, 0) # HTTP code
                assert "0" == elem(result, 2) # command result
 
+               # Correct OAuth call using specific ejabberd:user scope
+               token = EjabberdOauthMock.get_token @user, @domain, "ejabberd:user"
+               req = request(method: :GET,
+                                                                       path: ["api", @command],
+                                                                       q: [nokey: ""],
+                                                                       # OAuth
+                                                                       auth: {:oauth, token, []},
+                                                                       ip: {{127,0,0,1},60000},
+                                                                       host: @domain)
+               result = :mod_http_api.process([@command], req)
+               assert 200 == elem(result, 0) # HTTP code
+               assert "0" == elem(result, 2) # command result
+
                # Wrong OAuth token
                req = request(method: :GET,
                                                                        path: ["api", @command],
@@ -184,8 +197,8 @@ defmodule ModHttpApiMockTest do
                result = :mod_http_api.process([@command], req)
                assert 401 == elem(result, 0) # HTTP code
 
-               # Check that the command was executed only once
-               assert 1 ==
+               # Check that the command was executed twice
+               assert 2 ==
                        :meck.num_calls(:ejabberd_commands, :execute_command, :_)
 
                assert :meck.validate :ejabberd_auth