aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--include/ejabberd_commands.hrl2
-rw-r--r--src/ejabberd_commands.erl20
-rw-r--r--src/ejabberd_oauth.erl52
-rw-r--r--src/mod_http_api.erl20
-rw-r--r--test/mod_http_api_mock_test.exs27
5 files changed, 76 insertions, 45 deletions
diff --git a/include/ejabberd_commands.hrl b/include/ejabberd_commands.hrl
index 81be06dc3..2b4eca581 100644
--- a/include/ejabberd_commands.hrl
+++ b/include/ejabberd_commands.hrl
@@ -26,6 +26,8 @@
{tuple, [rterm()]} | {list, rterm()} |
rescode | restuple.
+-type oauth_scope() :: atom().
+
-record(ejabberd_commands,
{name :: atom(),
tags = [] :: [atom()] | '_' | '$2',
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl
index 9d41f50c2..075ff35cf 100644
--- a/src/ejabberd_commands.erl
+++ b/src/ejabberd_commands.erl
@@ -218,7 +218,7 @@
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 ->
diff --git a/src/ejabberd_oauth.erl b/src/ejabberd_oauth.erl
index 246bac127..0397571b2 100644
--- a/src/ejabberd_oauth.erl
+++ b/src/ejabberd_oauth.erl
@@ -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;
diff --git a/src/mod_http_api.erl b/src/mod_http_api.erl
index 1b4aa502b..f6621c09f 100644
--- a/src/mod_http_api.erl
+++ b/src/mod_http_api.erl
@@ -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
diff --git a/test/mod_http_api_mock_test.exs b/test/mod_http_api_mock_test.exs
index 47b1fe94a..db8761887 100644
--- a/test/mod_http_api_mock_test.exs
+++ b/test/mod_http_api_mock_test.exs
@@ -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