mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-20 16:15:59 +01:00
Add 'ejabberd:user' and 'ejabberd:admin' oauth scopes
'ejabberd:user' includes all commands defined with policy "user". 'ejabberd:admin' includes commands defined with policy "admin".
This commit is contained in:
parent
673a654c47
commit
33e0283f0d
@ -26,6 +26,8 @@
|
|||||||
{tuple, [rterm()]} | {list, rterm()} |
|
{tuple, [rterm()]} | {list, rterm()} |
|
||||||
rescode | restuple.
|
rescode | restuple.
|
||||||
|
|
||||||
|
-type oauth_scope() :: atom().
|
||||||
|
|
||||||
-record(ejabberd_commands,
|
-record(ejabberd_commands,
|
||||||
{name :: atom(),
|
{name :: atom(),
|
||||||
tags = [] :: [atom()] | '_' | '$2',
|
tags = [] :: [atom()] | '_' | '$2',
|
||||||
|
@ -218,7 +218,7 @@
|
|||||||
get_command_format/1,
|
get_command_format/1,
|
||||||
get_command_format/2,
|
get_command_format/2,
|
||||||
get_command_format/3,
|
get_command_format/3,
|
||||||
get_command_policy/1,
|
get_command_policy_and_scope/1,
|
||||||
get_command_definition/1,
|
get_command_definition/1,
|
||||||
get_command_definition/2,
|
get_command_definition/2,
|
||||||
get_tags_commands/0,
|
get_tags_commands/0,
|
||||||
@ -366,17 +366,23 @@ get_command_format(Name, Auth, Version) ->
|
|||||||
{Args, Result}
|
{Args, Result}
|
||||||
end.
|
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.
|
%% @doc return command policy.
|
||||||
get_command_policy(Name) ->
|
get_command_policy_and_scope(Name) ->
|
||||||
case get_command_definition(Name) of
|
case get_command_definition(Name) of
|
||||||
#ejabberd_commands{policy = Policy} ->
|
#ejabberd_commands{policy = Policy} = Cmd ->
|
||||||
{ok, Policy};
|
{ok, Policy, cmd_scope(Cmd)};
|
||||||
command_not_found ->
|
command_not_found ->
|
||||||
{error, command_not_found}
|
{error, command_not_found}
|
||||||
end.
|
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().
|
-spec get_command_definition(atom()) -> ejabberd_commands().
|
||||||
|
|
||||||
%% @doc Get the definition record of a command.
|
%% @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) ->
|
check_auth(_Command, noauth) ->
|
||||||
no_auth_provided;
|
no_auth_provided;
|
||||||
check_auth(Command, {User, Server, {oauth, Token}, _}) ->
|
check_auth(Command, {User, Server, {oauth, Token}, _}) ->
|
||||||
Scope = erlang:atom_to_binary(Command#ejabberd_commands.name, utf8),
|
ScopeList = cmd_scope(Command),
|
||||||
case ejabberd_oauth:check_token(User, Server, Scope, Token) of
|
case ejabberd_oauth:check_token(User, Server, ScopeList, Token) of
|
||||||
true ->
|
true ->
|
||||||
{ok, User, Server};
|
{ok, User, Server};
|
||||||
false ->
|
false ->
|
||||||
|
@ -90,7 +90,7 @@ start() ->
|
|||||||
get_commands_spec() ->
|
get_commands_spec() ->
|
||||||
[
|
[
|
||||||
#ejabberd_commands{name = oauth_issue_token, tags = [oauth],
|
#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,
|
module = ?MODULE, function = oauth_issue_token,
|
||||||
args = [{jid, string},{scopes, string}],
|
args = [{jid, string},{scopes, string}],
|
||||||
policy = restricted,
|
policy = restricted,
|
||||||
@ -106,11 +106,11 @@ get_commands_spec() ->
|
|||||||
result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}
|
result = {tokens, {list, {token, {tuple, [{token, string}, {user, string}, {scope, string}, {expires_in, string}]}}}}
|
||||||
},
|
},
|
||||||
#ejabberd_commands{name = oauth_list_scopes, tags = [oauth],
|
#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,
|
module = ?MODULE, function = oauth_list_scopes,
|
||||||
args = [],
|
args = [],
|
||||||
policy = restricted,
|
policy = restricted,
|
||||||
result = {scopes, {list, {scope, string}}}
|
result = {scopes, {list, {scope, {tuple, [{scope, string}, {commands, string}]}}}}
|
||||||
},
|
},
|
||||||
#ejabberd_commands{name = oauth_revoke_token, tags = [oauth],
|
#ejabberd_commands{name = oauth_revoke_token, tags = [oauth],
|
||||||
desc = "Revoke authorization for a token",
|
desc = "Revoke authorization for a token",
|
||||||
@ -153,7 +153,7 @@ oauth_revoke_token(Token) ->
|
|||||||
oauth_list_tokens().
|
oauth_list_tokens().
|
||||||
|
|
||||||
oauth_list_scopes() ->
|
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) ->
|
verify_resowner_scope({user, _User, _Server}, Scope, Ctx) ->
|
||||||
Cmds = ejabberd_commands:get_commands(),
|
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],
|
RegisteredScope = [atom_to_binary(C, utf8) || C <- Cmds1],
|
||||||
case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
|
case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
|
||||||
oauth2_priv_set:new(RegisteredScope)) of
|
oauth2_priv_set:new(RegisteredScope)) of
|
||||||
@ -254,17 +254,27 @@ verify_resowner_scope(_, _, _) ->
|
|||||||
|
|
||||||
|
|
||||||
get_cmd_scopes() ->
|
get_cmd_scopes() ->
|
||||||
Cmds = lists:filter(fun(Cmd) -> case ejabberd_commands:get_command_policy(Cmd) of
|
ScopeMap = lists:foldl(fun(Cmd, Accum) ->
|
||||||
{ok, Policy} when Policy =/= restricted -> true;
|
case ejabberd_commands:get_command_policy_and_scope(Cmd) of
|
||||||
_ -> false
|
{ok, Policy, Scopes} when Policy =/= restricted ->
|
||||||
end end,
|
lists:foldl(fun(Scope, Accum2) ->
|
||||||
ejabberd_commands:get_commands()),
|
dict:append(Scope, Cmd, Accum2)
|
||||||
[atom_to_binary(C, utf8) || C <- Cmds].
|
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
|
%% This is callback for oauth tokens generated through the command line. Only open and admin commands are
|
||||||
%% made available.
|
%% made available.
|
||||||
verify_client_scope({client, ejabberd_ctl}, Scope, Ctx) ->
|
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),
|
case oauth2_priv_set:is_subset(oauth2_priv_set:new(Scope),
|
||||||
oauth2_priv_set:new(RegisteredScope)) of
|
oauth2_priv_set:new(RegisteredScope)) of
|
||||||
true ->
|
true ->
|
||||||
@ -299,7 +309,7 @@ associate_refresh_token(_RefreshToken, _Context, AppContext) ->
|
|||||||
{ok, AppContext}.
|
{ok, AppContext}.
|
||||||
|
|
||||||
|
|
||||||
check_token(User, Server, Scope, Token) ->
|
check_token(User, Server, ScopeList, Token) ->
|
||||||
LUser = jid:nodeprep(User),
|
LUser = jid:nodeprep(User),
|
||||||
LServer = jid:nameprep(Server),
|
LServer = jid:nameprep(Server),
|
||||||
case catch mnesia:dirty_read(oauth_token, Token) of
|
case catch mnesia:dirty_read(oauth_token, Token) of
|
||||||
@ -308,23 +318,25 @@ check_token(User, Server, Scope, Token) ->
|
|||||||
expire = Expire}] ->
|
expire = Expire}] ->
|
||||||
{MegaSecs, Secs, _} = os:timestamp(),
|
{MegaSecs, Secs, _} = os:timestamp(),
|
||||||
TS = 1000000 * MegaSecs + Secs,
|
TS = 1000000 * MegaSecs + Secs,
|
||||||
oauth2_priv_set:is_member(
|
TokenScopeSet = oauth2_priv_set:new(TokenScope),
|
||||||
Scope, oauth2_priv_set:new(TokenScope)) andalso
|
lists:any(fun(Scope) ->
|
||||||
Expire > TS;
|
oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
|
||||||
|
ScopeList) andalso Expire > TS;
|
||||||
_ ->
|
_ ->
|
||||||
false
|
false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_token(Scope, Token) ->
|
check_token(ScopeList, Token) ->
|
||||||
case catch mnesia:dirty_read(oauth_token, Token) of
|
case catch mnesia:dirty_read(oauth_token, Token) of
|
||||||
[#oauth_token{us = US,
|
[#oauth_token{us = US,
|
||||||
scope = TokenScope,
|
scope = TokenScope,
|
||||||
expire = Expire}] ->
|
expire = Expire}] ->
|
||||||
{MegaSecs, Secs, _} = os:timestamp(),
|
{MegaSecs, Secs, _} = os:timestamp(),
|
||||||
TS = 1000000 * MegaSecs + Secs,
|
TS = 1000000 * MegaSecs + Secs,
|
||||||
case oauth2_priv_set:is_member(
|
TokenScopeSet = oauth2_priv_set:new(TokenScope),
|
||||||
Scope, oauth2_priv_set:new(TokenScope)) andalso
|
case lists:any(fun(Scope) ->
|
||||||
Expire > TS of
|
oauth2_priv_set:is_member(Scope, TokenScopeSet) end,
|
||||||
|
ScopeList) andalso Expire > TS of
|
||||||
true -> {ok, user, US};
|
true -> {ok, user, US};
|
||||||
false -> false
|
false -> false
|
||||||
end;
|
end;
|
||||||
|
@ -133,13 +133,13 @@ depends(_Host, _Opts) ->
|
|||||||
check_permissions(Request, Command) ->
|
check_permissions(Request, Command) ->
|
||||||
case catch binary_to_existing_atom(Command, utf8) of
|
case catch binary_to_existing_atom(Command, utf8) of
|
||||||
Call when is_atom(Call) ->
|
Call when is_atom(Call) ->
|
||||||
{ok, CommandPolicy} = ejabberd_commands:get_command_policy(Call),
|
{ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call),
|
||||||
check_permissions2(Request, Call, CommandPolicy);
|
check_permissions2(Request, Call, CommandPolicy, Scope);
|
||||||
_ ->
|
_ ->
|
||||||
unauthorized_response()
|
unauthorized_response()
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
|
check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList)
|
||||||
when HTTPAuth /= undefined ->
|
when HTTPAuth /= undefined ->
|
||||||
Admin =
|
Admin =
|
||||||
case lists:keysearch(<<"X-Admin">>, 1, Headers) of
|
case lists:keysearch(<<"X-Admin">>, 1, Headers) of
|
||||||
@ -159,7 +159,7 @@ check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
|
|||||||
false
|
false
|
||||||
end;
|
end;
|
||||||
{oauth, Token, _} ->
|
{oauth, Token, _} ->
|
||||||
case oauth_check_token(Call, Token) of
|
case oauth_check_token(ScopeList, Token) of
|
||||||
{ok, user, {User, Server}} ->
|
{ok, user, {User, Server}} ->
|
||||||
{ok, {User, Server, {oauth, Token}, Admin}};
|
{ok, {User, Server, {oauth, Token}, Admin}};
|
||||||
false ->
|
false ->
|
||||||
@ -172,9 +172,9 @@ check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _)
|
|||||||
{ok, A} -> {allowed, Call, A};
|
{ok, A} -> {allowed, Call, A};
|
||||||
_ -> unauthorized_response()
|
_ -> unauthorized_response()
|
||||||
end;
|
end;
|
||||||
check_permissions2(_Request, Call, open) ->
|
check_permissions2(_Request, Call, open, _Scope) ->
|
||||||
{allowed, Call, noauth};
|
{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,
|
Access = gen_mod:get_module_opt(global, ?MODULE, admin_ip_access,
|
||||||
fun(V) -> V end,
|
fun(V) -> V end,
|
||||||
none),
|
none),
|
||||||
@ -194,13 +194,11 @@ check_permissions2(#request{ip={IP, _Port}}, Call, _Policy) ->
|
|||||||
_E ->
|
_E ->
|
||||||
{allowed, Call, noauth}
|
{allowed, Call, noauth}
|
||||||
end;
|
end;
|
||||||
check_permissions2(_Request, _Call, _Policy) ->
|
check_permissions2(_Request, _Call, _Policy, _Scope) ->
|
||||||
unauthorized_response().
|
unauthorized_response().
|
||||||
|
|
||||||
oauth_check_token(Scope, Token) when is_atom(Scope) ->
|
oauth_check_token(ScopeList, Token) when is_list(ScopeList) ->
|
||||||
oauth_check_token(atom_to_binary(Scope, utf8), Token);
|
ejabberd_oauth:check_token(ScopeList, Token).
|
||||||
oauth_check_token(Scope, Token) ->
|
|
||||||
ejabberd_oauth:check_token(Scope, Token).
|
|
||||||
|
|
||||||
%% ------------------
|
%% ------------------
|
||||||
%% command processing
|
%% command processing
|
||||||
|
@ -70,8 +70,8 @@ defmodule ModHttpApiMockTest do
|
|||||||
fn (@acommand, {@user, @domain, @userpass, false}, @version) ->
|
fn (@acommand, {@user, @domain, @userpass, false}, @version) ->
|
||||||
{[], {:res, :rescode}}
|
{[], {:res, :rescode}}
|
||||||
end)
|
end)
|
||||||
:meck.expect(:ejabberd_commands, :get_command_policy,
|
:meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
|
||||||
fn (@acommand) -> {:ok, :user} end)
|
fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8)]} end)
|
||||||
:meck.expect(:ejabberd_commands, :get_commands,
|
:meck.expect(:ejabberd_commands, :get_commands,
|
||||||
fn () -> [@acommand] end)
|
fn () -> [@acommand] end)
|
||||||
:meck.expect(:ejabberd_commands, :execute_command,
|
:meck.expect(:ejabberd_commands, :execute_command,
|
||||||
@ -123,8 +123,8 @@ defmodule ModHttpApiMockTest do
|
|||||||
fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) ->
|
fn (@acommand, {@user, @domain, {:oauth, _token}, false}, @version) ->
|
||||||
{[], {:res, :rescode}}
|
{[], {:res, :rescode}}
|
||||||
end)
|
end)
|
||||||
:meck.expect(:ejabberd_commands, :get_command_policy,
|
:meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
|
||||||
fn (@acommand) -> {:ok, :user} end)
|
fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd:user"]} end)
|
||||||
:meck.expect(:ejabberd_commands, :get_commands,
|
:meck.expect(:ejabberd_commands, :get_commands,
|
||||||
fn () -> [@acommand] end)
|
fn () -> [@acommand] end)
|
||||||
:meck.expect(:ejabberd_commands, :execute_command,
|
:meck.expect(:ejabberd_commands, :execute_command,
|
||||||
@ -134,7 +134,7 @@ defmodule ModHttpApiMockTest do
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
||||||
# Correct OAuth call
|
# Correct OAuth call using specific scope
|
||||||
token = EjabberdOauthMock.get_token @user, @domain, @command
|
token = EjabberdOauthMock.get_token @user, @domain, @command
|
||||||
req = request(method: :GET,
|
req = request(method: :GET,
|
||||||
path: ["api", @command],
|
path: ["api", @command],
|
||||||
@ -147,6 +147,19 @@ defmodule ModHttpApiMockTest do
|
|||||||
assert 200 == elem(result, 0) # HTTP code
|
assert 200 == elem(result, 0) # HTTP code
|
||||||
assert "0" == elem(result, 2) # command result
|
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
|
# Wrong OAuth token
|
||||||
req = request(method: :GET,
|
req = request(method: :GET,
|
||||||
path: ["api", @command],
|
path: ["api", @command],
|
||||||
@ -184,8 +197,8 @@ defmodule ModHttpApiMockTest do
|
|||||||
result = :mod_http_api.process([@command], req)
|
result = :mod_http_api.process([@command], req)
|
||||||
assert 401 == elem(result, 0) # HTTP code
|
assert 401 == elem(result, 0) # HTTP code
|
||||||
|
|
||||||
# Check that the command was executed only once
|
# Check that the command was executed twice
|
||||||
assert 1 ==
|
assert 2 ==
|
||||||
:meck.num_calls(:ejabberd_commands, :execute_command, :_)
|
:meck.num_calls(:ejabberd_commands, :execute_command, :_)
|
||||||
|
|
||||||
assert :meck.validate :ejabberd_auth
|
assert :meck.validate :ejabberd_auth
|
||||||
|
Loading…
Reference in New Issue
Block a user