mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-24 16:23:40 +01:00
Merge pull request #1211 from processone/expand_api
There is still work to do, be we reached a stable state and can merge up to this point.
This commit is contained in:
commit
7a74a4836a
@ -28,6 +28,23 @@
|
|||||||
|
|
||||||
-type oauth_scope() :: atom().
|
-type oauth_scope() :: atom().
|
||||||
|
|
||||||
|
%% ejabberd_commands OAuth ReST ACL definition:
|
||||||
|
%% Two fields exist that are used to control access on a command from ReST API:
|
||||||
|
%% 1. Policy
|
||||||
|
%% If policy is:
|
||||||
|
%% - restricted: command is not exposed as OAuth Rest API.
|
||||||
|
%% - admin: Command is allowed for user that have Admin Rest command enabled by access rule: commands_admin_access
|
||||||
|
%% - user: Command might be called by any server user.
|
||||||
|
%% - open: Command can be called by anyone.
|
||||||
|
%%
|
||||||
|
%% Policy is just used to control who can call the command. A specific additional access rules can be performed, as
|
||||||
|
%% defined by access option.
|
||||||
|
%% Access option can be a list of:
|
||||||
|
%% - {Module, accessName, DefaultValue}: Reference and existing module access to limit who can use the command.
|
||||||
|
%% - AccessRule name: direct name of the access rule to check in config file.
|
||||||
|
%% TODO: Access option could be atom command (not a list). In the case, User performing the command, will be added as first parameter
|
||||||
|
%% to command, so that the command can perform additional check.
|
||||||
|
|
||||||
-record(ejabberd_commands,
|
-record(ejabberd_commands,
|
||||||
{name :: atom(),
|
{name :: atom(),
|
||||||
tags = [] :: [atom()] | '_' | '$2',
|
tags = [] :: [atom()] | '_' | '$2',
|
||||||
@ -38,19 +55,25 @@
|
|||||||
function :: atom() | '_',
|
function :: atom() | '_',
|
||||||
args = [] :: [aterm()] | '_' | '$1' | '$2',
|
args = [] :: [aterm()] | '_' | '$1' | '$2',
|
||||||
policy = restricted :: open | restricted | admin | user,
|
policy = restricted :: open | restricted | admin | user,
|
||||||
|
%% access is: [accessRuleName] or [{Module, AccessOption, DefaultAccessRuleName}]
|
||||||
|
access = [] :: [{atom(),atom(),atom()}|atom()],
|
||||||
result = {res, rescode} :: rterm() | '_' | '$2',
|
result = {res, rescode} :: rterm() | '_' | '$2',
|
||||||
args_desc = none :: none | [string()] | '_',
|
args_desc = none :: none | [string()] | '_',
|
||||||
result_desc = none :: none | string() | '_',
|
result_desc = none :: none | string() | '_',
|
||||||
args_example = none :: none | [any()] | '_',
|
args_example = none :: none | [any()] | '_',
|
||||||
result_example = none :: any()}).
|
result_example = none :: any()}).
|
||||||
|
|
||||||
|
%% TODO Fix me: Type is not up to date
|
||||||
-type ejabberd_commands() :: #ejabberd_commands{name :: atom(),
|
-type ejabberd_commands() :: #ejabberd_commands{name :: atom(),
|
||||||
tags :: [atom()],
|
tags :: [atom()],
|
||||||
desc :: string(),
|
desc :: string(),
|
||||||
longdesc :: string(),
|
longdesc :: string(),
|
||||||
|
version :: integer(),
|
||||||
module :: atom(),
|
module :: atom(),
|
||||||
function :: atom(),
|
function :: atom(),
|
||||||
args :: [aterm()],
|
args :: [aterm()],
|
||||||
|
policy :: open | restricted | admin | user,
|
||||||
|
access :: [{atom(),atom(),atom()}|atom()],
|
||||||
result :: rterm()}.
|
result :: rterm()}.
|
||||||
|
|
||||||
%% @type ejabberd_commands() = #ejabberd_commands{
|
%% @type ejabberd_commands() = #ejabberd_commands{
|
||||||
|
@ -3,7 +3,7 @@ defmodule ExUnit.CTFormatter do
|
|||||||
|
|
||||||
use GenEvent
|
use GenEvent
|
||||||
|
|
||||||
import ExUnit.Formatter, only: [format_time: 2, format_filters: 2, format_test_failure: 5,
|
import ExUnit.Formatter, only: [format_time: 2, format_test_failure: 5,
|
||||||
format_test_case_failure: 5]
|
format_test_case_failure: 5]
|
||||||
|
|
||||||
def init(opts) do
|
def init(opts) do
|
||||||
|
6
mix.exs
6
mix.exs
@ -11,6 +11,8 @@ defmodule Ejabberd.Mixfile do
|
|||||||
compilers: [:asn1] ++ Mix.compilers,
|
compilers: [:asn1] ++ Mix.compilers,
|
||||||
erlc_options: erlc_options,
|
erlc_options: erlc_options,
|
||||||
erlc_paths: ["asn1", "src"],
|
erlc_paths: ["asn1", "src"],
|
||||||
|
# Elixir tests are starting the part of ejabberd they need
|
||||||
|
aliases: [test: "test --no-start"],
|
||||||
package: package,
|
package: package,
|
||||||
deps: deps]
|
deps: deps]
|
||||||
end
|
end
|
||||||
@ -59,7 +61,9 @@ defmodule Ejabberd.Mixfile do
|
|||||||
{:exrm, "~> 1.0.0", only: :dev},
|
{:exrm, "~> 1.0.0", only: :dev},
|
||||||
# relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with
|
# relx is used by exrm. Lock version as for now, ejabberd doesn not compile fine with
|
||||||
# version 3.20:
|
# version 3.20:
|
||||||
{:relx, "~> 3.19.0", only: :dev}]
|
{:relx, "~> 3.19.0", only: :dev},
|
||||||
|
{:meck, "~> 0.8.4", only: :test},
|
||||||
|
{:moka, github: "processone/moka", tag: "1.0.5b", only: :test}]
|
||||||
end
|
end
|
||||||
|
|
||||||
defp package do
|
defp package do
|
||||||
|
9
mix.lock
9
mix.lock
@ -3,7 +3,7 @@
|
|||||||
"cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []},
|
"cf": {:hex, :cf, "0.2.1", "69d0b1349fd4d7d4dc55b7f407d29d7a840bf9a1ef5af529f1ebe0ce153fc2ab", [:rebar3], []},
|
||||||
"eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []},
|
"eredis": {:hex, :eredis, "1.0.8", "ab4fda1c4ba7fbe6c19c26c249dc13da916d762502c4b4fa2df401a8d51c5364", [:rebar], []},
|
||||||
"erlware_commons": {:hex, :erlware_commons, "0.19.0", "7b43caf2c91950c5f60dc20451e3c3afba44d3d4f7f27bcdc52469285a5a3e70", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]},
|
"erlware_commons": {:hex, :erlware_commons, "0.19.0", "7b43caf2c91950c5f60dc20451e3c3afba44d3d4f7f27bcdc52469285a5a3e70", [:rebar3], [{:cf, "0.2.1", [hex: :cf, optional: false]}]},
|
||||||
"esip": {:hex, :esip, "1.0.7", "f75f6a5cac6814e506f0ff96141fbe276dee3261fca1471c8edfdde25b74f877", [:rebar3], [{:stun, "1.0.6", [hex: :stun, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}]},
|
"esip": {:hex, :esip, "1.0.7", "f75f6a5cac6814e506f0ff96141fbe276dee3261fca1471c8edfdde25b74f877", [:rebar3], [{:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:stun, "1.0.6", [hex: :stun, optional: false]}]},
|
||||||
"exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]},
|
"exrm": {:hex, :exrm, "1.0.8", "5aa8990cdfe300282828b02cefdc339e235f7916388ce99f9a1f926a9271a45d", [:mix], [{:relx, "~> 3.5", [hex: :relx, optional: false]}]},
|
||||||
"ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []},
|
"ezlib": {:hex, :ezlib, "1.0.1", "add8b2770a1a70c174aaea082b4a8668c0c7fdb03ee6cc81c6c68d3a6c3d767d", [:rebar3], []},
|
||||||
"fast_tls": {:hex, :fast_tls, "1.0.6", "750a74aabb05056f0f222910f0955883649e6c5d67df6ca504ff676160d22b89", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
|
"fast_tls": {:hex, :fast_tls, "1.0.6", "750a74aabb05056f0f222910f0955883649e6c5d67df6ca504ff676160d22b89", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
|
||||||
@ -14,13 +14,16 @@
|
|||||||
"iconv": {:hex, :iconv, "1.0.1", "dbb8700070577e7a021a095cc5ead221069a0c4034bfadca2516c1f1109ee7fd", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
|
"iconv": {:hex, :iconv, "1.0.1", "dbb8700070577e7a021a095cc5ead221069a0c4034bfadca2516c1f1109ee7fd", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
|
||||||
"jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []},
|
"jiffy": {:hex, :jiffy, "0.14.7", "9f33b893edd6041ceae03bc1e50b412e858cc80b46f3d7535a7a9940a79a1c37", [:rebar, :make], []},
|
||||||
"lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]},
|
"lager": {:hex, :lager, "3.0.2", "25dc81bc3659b62f5ab9bd073e97ddd894fc4c242019fccef96f3889d7366c97", [:rebar3], [{:goldrush, "0.1.7", [hex: :goldrush, optional: false]}]},
|
||||||
|
"meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []},
|
||||||
|
"moka": {:git, "https://github.com/processone/moka.git", "768efea96443c57125e6247dbebee687f17be149", [tag: "1.0.5b"]},
|
||||||
"p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []},
|
"p1_mysql": {:hex, :p1_mysql, "1.0.1", "d2be1cfc71bb4f1391090b62b74c3f5cb8e7a45b0076b8cb290cd6b2856c581b", [:rebar3], []},
|
||||||
"p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []},
|
"p1_oauth2": {:hex, :p1_oauth2, "0.6.1", "4e021250cc198c538b097393671a41e7cebf463c248980320e038fe0316eb56b", [:rebar3], []},
|
||||||
"p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []},
|
"p1_pgsql": {:hex, :p1_pgsql, "1.1.0", "ca525c42878eac095e5feb19563acc9915c845648f48fdec7ba6266c625d4ac7", [:rebar3], []},
|
||||||
"p1_utils": {:hex, :p1_utils, "1.0.4", "7face65db102b5d1ebe7ad3c7517c5ee8cfbe174c6658e3affbb00eb66e06787", [:rebar3], []},
|
"p1_utils": {:hex, :p1_utils, "1.0.4", "7face65db102b5d1ebe7ad3c7517c5ee8cfbe174c6658e3affbb00eb66e06787", [:rebar3], []},
|
||||||
"p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []},
|
"p1_xmlrpc": {:hex, :p1_xmlrpc, "1.15.1", "a382b62dc21bb372281c2488f99294d84f2b4020ed0908a1c4ad710ace3cf35a", [:rebar3], []},
|
||||||
"providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]},
|
"providers": {:hex, :providers, "1.6.0", "db0e2f9043ae60c0155205fcd238d68516331d0e5146155e33d1e79dc452964a", [:rebar3], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]},
|
||||||
"relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:providers, "1.6.0", [hex: :providers, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}]},
|
"relx": {:hex, :relx, "3.19.0", "286dd5244b4786f56aac75d5c8e2d1fb4cfd306810d4ec8548f3ae1b3aadb8f7", [:rebar3], [{:bbmustache, "1.0.4", [hex: :bbmustache, optional: false]}, {:cf, "0.2.1", [hex: :cf, optional: false]}, {:erlware_commons, "0.19.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.6.0", [hex: :providers, optional: false]}]},
|
||||||
|
"samerlib": {:git, "https://github.com/processone/samerlib", "9158f65d18ec63f8b409543b6fb46dd5fce46160", [tag: "0.8.0b"]},
|
||||||
"sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []},
|
"sqlite3": {:hex, :sqlite3, "1.1.5", "794738b6d07b6d36ec6d42492cb9d629bad9cf3761617b8b8d728e765db19840", [:rebar3], []},
|
||||||
"stringprep": {:hex, :stringprep, "1.0.5", "f29395275c35af5051b29bf875b44ac632dc4d0287880f0e143b536c61fd0ed5", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
|
"stringprep": {:hex, :stringprep, "1.0.5", "f29395275c35af5051b29bf875b44ac632dc4d0287880f0e143b536c61fd0ed5", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]},
|
||||||
"stun": {:hex, :stun, "1.0.6", "1ca9dea574e09f60971bd8de9cb7e34f327cbf435462cf56aa30f05c1ee2f231", [:rebar3], [{:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}, {:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}]}}
|
"stun": {:hex, :stun, "1.0.6", "1ca9dea574e09f60971bd8de9cb7e34f327cbf435462cf56aa30f05c1ee2f231", [:rebar3], [{:fast_tls, "1.0.6", [hex: :fast_tls, optional: false]}, {:p1_utils, "1.0.4", [hex: :p1_utils, optional: false]}]}}
|
||||||
|
11
src/acl.erl
11
src/acl.erl
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
-export([add_access/3, clear/0]).
|
-export([add_access/3, clear/0]).
|
||||||
-export([start/0, add/3, add_list/3, add_local/3, add_list_local/3,
|
-export([start/0, add/3, add_list/3, add_local/3, add_list_local/3,
|
||||||
load_from_config/0, match_rule/3,
|
load_from_config/0, match_rule/3, any_rules_allowed/3,
|
||||||
transform_options/1, opt_type/1, acl_rule_matches/3,
|
transform_options/1, opt_type/1, acl_rule_matches/3,
|
||||||
acl_rule_verify/1, access_matches/3,
|
acl_rule_verify/1, access_matches/3,
|
||||||
transform_access_rules_config/1,
|
transform_access_rules_config/1,
|
||||||
@ -275,6 +275,15 @@ normalize_spec(Spec) ->
|
|||||||
end
|
end
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
-spec any_rules_allowed(global | binary(), access_name(),
|
||||||
|
jid() | ljid() | inet:ip_address()) -> boolean().
|
||||||
|
|
||||||
|
any_rules_allowed(Host, Access, Entity) ->
|
||||||
|
lists:any(fun (Rule) ->
|
||||||
|
allow == acl:match_rule(Host, Rule, Entity)
|
||||||
|
end,
|
||||||
|
Access).
|
||||||
|
|
||||||
-spec match_rule(global | binary(), access_name(),
|
-spec match_rule(global | binary(), access_name(),
|
||||||
jid() | ljid() | inet:ip_address()) -> any().
|
jid() | ljid() | inet:ip_address()) -> any().
|
||||||
|
|
||||||
|
@ -129,6 +129,8 @@ get_commands_spec() ->
|
|||||||
|
|
||||||
#ejabberd_commands{name = register, tags = [accounts],
|
#ejabberd_commands{name = register, tags = [accounts],
|
||||||
desc = "Register a user",
|
desc = "Register a user",
|
||||||
|
policy = admin,
|
||||||
|
access = [{mod_register, access, configure}],
|
||||||
module = ?MODULE, function = register,
|
module = ?MODULE, function = register,
|
||||||
args = [{user, binary}, {host, binary}, {password, binary}],
|
args = [{user, binary}, {host, binary}, {password, binary}],
|
||||||
result = {res, restuple}},
|
result = {res, restuple}},
|
||||||
@ -166,7 +168,7 @@ get_commands_spec() ->
|
|||||||
#ejabberd_commands{name = list_cluster, tags = [cluster],
|
#ejabberd_commands{name = list_cluster, tags = [cluster],
|
||||||
desc = "List nodes that are part of the cluster handled by Node",
|
desc = "List nodes that are part of the cluster handled by Node",
|
||||||
module = ?MODULE, function = list_cluster,
|
module = ?MODULE, function = list_cluster,
|
||||||
args = [],
|
args = [],
|
||||||
result = {nodes, {list, {node, atom}}}},
|
result = {nodes, {list, {node, atom}}}},
|
||||||
|
|
||||||
#ejabberd_commands{name = import_file, tags = [mnesia],
|
#ejabberd_commands{name = import_file, tags = [mnesia],
|
||||||
@ -220,7 +222,7 @@ get_commands_spec() ->
|
|||||||
desc = "Delete offline messages older than DAYS",
|
desc = "Delete offline messages older than DAYS",
|
||||||
module = ?MODULE, function = delete_old_messages,
|
module = ?MODULE, function = delete_old_messages,
|
||||||
args = [{days, integer}], result = {res, rescode}},
|
args = [{days, integer}], result = {res, rescode}},
|
||||||
|
|
||||||
#ejabberd_commands{name = export2sql, tags = [mnesia],
|
#ejabberd_commands{name = export2sql, tags = [mnesia],
|
||||||
desc = "Export virtual host information from Mnesia tables to SQL files",
|
desc = "Export virtual host information from Mnesia tables to SQL files",
|
||||||
module = ejd2sql, function = export,
|
module = ejd2sql, function = export,
|
||||||
|
@ -223,9 +223,10 @@
|
|||||||
get_command_definition/2,
|
get_command_definition/2,
|
||||||
get_tags_commands/0,
|
get_tags_commands/0,
|
||||||
get_tags_commands/1,
|
get_tags_commands/1,
|
||||||
get_commands/0,
|
get_exposed_commands/0,
|
||||||
register_commands/1,
|
register_commands/1,
|
||||||
unregister_commands/1,
|
unregister_commands/1,
|
||||||
|
expose_commands/1,
|
||||||
execute_command/2,
|
execute_command/2,
|
||||||
execute_command/3,
|
execute_command/3,
|
||||||
execute_command/4,
|
execute_command/4,
|
||||||
@ -275,10 +276,10 @@ get_commands_spec() ->
|
|||||||
init() ->
|
init() ->
|
||||||
mnesia:delete_table(ejabberd_commands),
|
mnesia:delete_table(ejabberd_commands),
|
||||||
mnesia:create_table(ejabberd_commands,
|
mnesia:create_table(ejabberd_commands,
|
||||||
[{ram_copies, [node()]},
|
[{ram_copies, [node()]},
|
||||||
{local_content, true},
|
{local_content, true},
|
||||||
{attributes, record_info(fields, ejabberd_commands)},
|
{attributes, record_info(fields, ejabberd_commands)},
|
||||||
{type, bag}]),
|
{type, bag}]),
|
||||||
mnesia:add_table_copy(ejabberd_commands, node(), ram_copies),
|
mnesia:add_table_copy(ejabberd_commands, node(), ram_copies),
|
||||||
register_commands(get_commands_spec()).
|
register_commands(get_commands_spec()).
|
||||||
|
|
||||||
@ -287,12 +288,14 @@ init() ->
|
|||||||
%% @doc Register ejabberd commands.
|
%% @doc Register ejabberd commands.
|
||||||
%% If a command is already registered, a warning is printed and the
|
%% If a command is already registered, a warning is printed and the
|
||||||
%% old command is preserved.
|
%% old command is preserved.
|
||||||
|
%% A registered command is not directly available to be called through
|
||||||
|
%% ejabberd ReST API. It need to be exposed to be available through API.
|
||||||
register_commands(Commands) ->
|
register_commands(Commands) ->
|
||||||
lists:foreach(
|
lists:foreach(
|
||||||
fun(Command) ->
|
fun(Command) ->
|
||||||
% XXX check if command exists
|
%% XXX check if command exists
|
||||||
mnesia:dirty_write(Command)
|
mnesia:dirty_write(Command)
|
||||||
% ?DEBUG("This command is already defined:~n~p", [Command])
|
%% ?DEBUG("This command is already defined:~n~p", [Command])
|
||||||
end,
|
end,
|
||||||
Commands).
|
Commands).
|
||||||
|
|
||||||
@ -306,6 +309,25 @@ unregister_commands(Commands) ->
|
|||||||
end,
|
end,
|
||||||
Commands).
|
Commands).
|
||||||
|
|
||||||
|
%% @doc Expose command through ejabberd ReST API.
|
||||||
|
%% Pass a list of command names or policy to expose.
|
||||||
|
-spec expose_commands([ejabberd_commands()|atom()|open|user|admin|restricted]) -> ok | {error, atom()}.
|
||||||
|
|
||||||
|
expose_commands(Commands) ->
|
||||||
|
Names = lists:map(fun(#ejabberd_commands{name = Name}) ->
|
||||||
|
Name;
|
||||||
|
(Name) when is_atom(Name) ->
|
||||||
|
Name
|
||||||
|
end,
|
||||||
|
Commands),
|
||||||
|
|
||||||
|
case ejabberd_config:add_local_option(commands, [{add_commands, Names}]) of
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason};
|
||||||
|
{atomic, Result} ->
|
||||||
|
Result
|
||||||
|
end.
|
||||||
|
|
||||||
-spec list_commands() -> [{atom(), [aterm()], string()}].
|
-spec list_commands() -> [{atom(), [aterm()], string()}].
|
||||||
|
|
||||||
%% @doc Get a list of all the available commands, arguments and description.
|
%% @doc Get a list of all the available commands, arguments and description.
|
||||||
@ -319,8 +341,8 @@ list_commands() ->
|
|||||||
list_commands(Version) ->
|
list_commands(Version) ->
|
||||||
Commands = get_commands_definition(Version),
|
Commands = get_commands_definition(Version),
|
||||||
[{Name, Args, Desc} || #ejabberd_commands{name = Name,
|
[{Name, Args, Desc} || #ejabberd_commands{name = Name,
|
||||||
args = Args,
|
args = Args,
|
||||||
desc = Desc} <- Commands].
|
desc = Desc} <- Commands].
|
||||||
|
|
||||||
|
|
||||||
-spec list_commands_policy(integer()) ->
|
-spec list_commands_policy(integer()) ->
|
||||||
@ -331,10 +353,10 @@ list_commands(Version) ->
|
|||||||
list_commands_policy(Version) ->
|
list_commands_policy(Version) ->
|
||||||
Commands = get_commands_definition(Version),
|
Commands = get_commands_definition(Version),
|
||||||
[{Name, Args, Desc, Policy} ||
|
[{Name, Args, Desc, Policy} ||
|
||||||
#ejabberd_commands{name = Name,
|
#ejabberd_commands{name = Name,
|
||||||
args = Args,
|
args = Args,
|
||||||
desc = Desc,
|
desc = Desc,
|
||||||
policy = Policy} <- Commands].
|
policy = Policy} <- Commands].
|
||||||
|
|
||||||
-spec get_command_format(atom()) -> {[aterm()], rterm()}.
|
-spec get_command_format(atom()) -> {[aterm()], rterm()}.
|
||||||
|
|
||||||
@ -356,14 +378,14 @@ get_command_format(Name, Auth, Version) ->
|
|||||||
Admin = is_admin(Name, Auth, #{}),
|
Admin = is_admin(Name, Auth, #{}),
|
||||||
#ejabberd_commands{args = Args,
|
#ejabberd_commands{args = Args,
|
||||||
result = Result,
|
result = Result,
|
||||||
policy = Policy} =
|
policy = Policy} =
|
||||||
get_command_definition(Name, Version),
|
get_command_definition(Name, Version),
|
||||||
case Policy of
|
case Policy of
|
||||||
user when Admin;
|
user when Admin;
|
||||||
Auth == noauth ->
|
Auth == noauth ->
|
||||||
{[{user, binary}, {server, binary} | Args], Result};
|
{[{user, binary}, {server, binary} | Args], Result};
|
||||||
_ ->
|
_ ->
|
||||||
{Args, Result}
|
{Args, Result}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}.
|
-spec get_command_policy_and_scope(atom()) -> {ok, open|user|admin|restricted, [oauth_scope()]} | {error, command_not_found}.
|
||||||
@ -394,16 +416,16 @@ get_command_definition(Name) ->
|
|||||||
%% @doc Get the definition record of a command in a given API version.
|
%% @doc Get the definition record of a command in a given API version.
|
||||||
get_command_definition(Name, Version) ->
|
get_command_definition(Name, Version) ->
|
||||||
case lists:reverse(
|
case lists:reverse(
|
||||||
lists:sort(
|
lists:sort(
|
||||||
mnesia:dirty_select(
|
mnesia:dirty_select(
|
||||||
ejabberd_commands,
|
ejabberd_commands,
|
||||||
ets:fun2ms(
|
ets:fun2ms(
|
||||||
fun(#ejabberd_commands{name = N, version = V} = C)
|
fun(#ejabberd_commands{name = N, version = V} = C)
|
||||||
when N == Name, V =< Version ->
|
when N == Name, V =< Version ->
|
||||||
{V, C}
|
{V, C}
|
||||||
end)))) of
|
end)))) of
|
||||||
[{_, Command} | _ ] -> Command;
|
[{_, Command} | _ ] -> Command;
|
||||||
_E -> throw(unknown_command)
|
_E -> throw(unknown_command)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
|
-spec get_commands_definition(integer()) -> [ejabberd_commands()].
|
||||||
@ -411,20 +433,20 @@ get_command_definition(Name, Version) ->
|
|||||||
% @doc Returns all commands for a given API version
|
% @doc Returns all commands for a given API version
|
||||||
get_commands_definition(Version) ->
|
get_commands_definition(Version) ->
|
||||||
L = lists:reverse(
|
L = lists:reverse(
|
||||||
lists:sort(
|
lists:sort(
|
||||||
mnesia:dirty_select(
|
mnesia:dirty_select(
|
||||||
ejabberd_commands,
|
ejabberd_commands,
|
||||||
ets:fun2ms(
|
ets:fun2ms(
|
||||||
fun(#ejabberd_commands{name = Name, version = V} = C)
|
fun(#ejabberd_commands{name = Name, version = V} = C)
|
||||||
when V =< Version ->
|
when V =< Version ->
|
||||||
{Name, V, C}
|
{Name, V, C}
|
||||||
end)))),
|
end)))),
|
||||||
F = fun({_Name, _V, Command}, []) ->
|
F = fun({_Name, _V, Command}, []) ->
|
||||||
[Command];
|
[Command];
|
||||||
({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
|
({Name, _V, _Command}, [#ejabberd_commands{name=Name}|_T] = Acc) ->
|
||||||
Acc;
|
Acc;
|
||||||
({_Name, _V, Command}, Acc) -> [Command | Acc]
|
({_Name, _V, Command}, Acc) -> [Command | Acc]
|
||||||
end,
|
end,
|
||||||
lists:foldl(F, [], L).
|
lists:foldl(F, [], L).
|
||||||
|
|
||||||
%% @spec (Name::atom(), Arguments) -> ResultTerm
|
%% @spec (Name::atom(), Arguments) -> ResultTerm
|
||||||
@ -433,7 +455,7 @@ get_commands_definition(Version) ->
|
|||||||
%% @doc Execute a command.
|
%% @doc Execute a command.
|
||||||
%% Can return the following exceptions:
|
%% Can return the following exceptions:
|
||||||
%% command_unknown | account_unprivileged | invalid_account_data |
|
%% command_unknown | account_unprivileged | invalid_account_data |
|
||||||
%% no_auth_provided
|
%% no_auth_provided | access_rules_unauthorized
|
||||||
execute_command(Name, Arguments) ->
|
execute_command(Name, Arguments) ->
|
||||||
execute_command(Name, Arguments, ?DEFAULT_VERSION).
|
execute_command(Name, Arguments, ?DEFAULT_VERSION).
|
||||||
|
|
||||||
@ -494,41 +516,62 @@ execute_command(AccessCommands, Auth, Name, Arguments) ->
|
|||||||
%%
|
%%
|
||||||
%% @doc Execute a command in a given API version
|
%% @doc Execute a command in a given API version
|
||||||
%% Can return the following exceptions:
|
%% Can return the following exceptions:
|
||||||
%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided
|
%% command_unknown | account_unprivileged | invalid_account_data | no_auth_provided | access_rules_unauthorized
|
||||||
execute_command(AccessCommands1, Auth1, Name, Arguments, Version) ->
|
execute_command(AccessCommands1, Auth1, Name, Arguments, Version) ->
|
||||||
execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}).
|
execute_command(AccessCommands1, Auth1, Name, Arguments, Version, #{}).
|
||||||
|
|
||||||
execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) ->
|
execute_command(AccessCommands1, Auth1, Name, Arguments, Version, CallerInfo) ->
|
||||||
Auth = case is_admin(Name, Auth1, CallerInfo) of
|
Auth = case is_admin(Name, Auth1, CallerInfo) of
|
||||||
true -> admin;
|
true -> admin;
|
||||||
false -> Auth1
|
false -> Auth1
|
||||||
end,
|
end,
|
||||||
|
TokenJID = oauth_token_user(Auth1),
|
||||||
Command = get_command_definition(Name, Version),
|
Command = get_command_definition(Name, Version),
|
||||||
AccessCommands = get_access_commands(AccessCommands1, Version),
|
AccessCommands = get_all_access_commands(AccessCommands1),
|
||||||
|
|
||||||
case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of
|
case check_access_commands(AccessCommands, Auth, Name, Command, Arguments, CallerInfo) of
|
||||||
ok -> execute_command2(Auth, Command, Arguments)
|
ok -> execute_check_policy(Auth, TokenJID, Command, Arguments)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
execute_command2(
|
|
||||||
_Auth, #ejabberd_commands{policy = open} = Command, Arguments) ->
|
|
||||||
execute_command2(Command, Arguments);
|
|
||||||
execute_command2(
|
|
||||||
_Auth, #ejabberd_commands{policy = restricted} = Command, Arguments) ->
|
|
||||||
execute_command2(Command, Arguments);
|
|
||||||
execute_command2(
|
|
||||||
_Auth, #ejabberd_commands{policy = admin} = Command, Arguments) ->
|
|
||||||
execute_command2(Command, Arguments);
|
|
||||||
execute_command2(
|
|
||||||
admin, #ejabberd_commands{policy = user} = Command, Arguments) ->
|
|
||||||
execute_command2(Command, Arguments);
|
|
||||||
execute_command2(
|
|
||||||
noauth, #ejabberd_commands{policy = user} = Command, Arguments) ->
|
|
||||||
execute_command2(Command, Arguments);
|
|
||||||
execute_command2(
|
|
||||||
{User, Server, _, _}, #ejabberd_commands{policy = user} = Command, Arguments) ->
|
|
||||||
execute_command2(Command, [User, Server | Arguments]).
|
|
||||||
|
|
||||||
execute_command2(Command, Arguments) ->
|
execute_check_policy(
|
||||||
|
_Auth, _JID, #ejabberd_commands{policy = open} = Command, Arguments) ->
|
||||||
|
do_execute_command(Command, Arguments);
|
||||||
|
execute_check_policy(
|
||||||
|
_Auth, _JID, #ejabberd_commands{policy = restricted} = Command, Arguments) ->
|
||||||
|
do_execute_command(Command, Arguments);
|
||||||
|
execute_check_policy(
|
||||||
|
_Auth, JID, #ejabberd_commands{policy = admin} = Command, Arguments) ->
|
||||||
|
execute_check_access(JID, Command, Arguments);
|
||||||
|
execute_check_policy(
|
||||||
|
admin, JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
|
||||||
|
execute_check_access(JID, Command, Arguments);
|
||||||
|
execute_check_policy(
|
||||||
|
noauth, _JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
|
||||||
|
do_execute_command(Command, Arguments);
|
||||||
|
execute_check_policy(
|
||||||
|
{User, Server, _, _}, JID, #ejabberd_commands{policy = user} = Command, Arguments) ->
|
||||||
|
execute_check_access(JID, Command, [User, Server | Arguments]).
|
||||||
|
|
||||||
|
execute_check_access(_FromJID, #ejabberd_commands{access = []} = Command, Arguments) ->
|
||||||
|
do_execute_command(Command, Arguments);
|
||||||
|
execute_check_access(FromJID, #ejabberd_commands{access = AccessRefs} = Command, Arguments) ->
|
||||||
|
%% TODO Review: Do we have smarter / better way to check rule on other Host than global ?
|
||||||
|
Host = global,
|
||||||
|
Rules = lists:map(fun({Mod, AccessName, Default}) ->
|
||||||
|
gen_mod:get_module_opt(Host, Mod,
|
||||||
|
AccessName, fun(A) -> A end, Default);
|
||||||
|
(Default) ->
|
||||||
|
Default
|
||||||
|
end, AccessRefs),
|
||||||
|
case acl:any_rules_allowed(Host, Rules, FromJID) of
|
||||||
|
true ->
|
||||||
|
do_execute_command(Command, Arguments);
|
||||||
|
false ->
|
||||||
|
throw({error, access_rules_unauthorized})
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_execute_command(Command, Arguments) ->
|
||||||
Module = Command#ejabberd_commands.module,
|
Module = Command#ejabberd_commands.module,
|
||||||
Function = Command#ejabberd_commands.function,
|
Function = Command#ejabberd_commands.function,
|
||||||
?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]),
|
?DEBUG("Executing command ~p:~p with Args=~p", [Module, Function, Arguments]),
|
||||||
@ -598,31 +641,31 @@ check_access_commands(AccessCommands, Auth, Method, Command1, Arguments, CallerI
|
|||||||
Command1
|
Command1
|
||||||
end,
|
end,
|
||||||
AccessCommandsAllowed =
|
AccessCommandsAllowed =
|
||||||
lists:filter(
|
lists:filter(
|
||||||
fun({Access, Commands, ArgumentRestrictions}) ->
|
fun({Access, Commands, ArgumentRestrictions}) ->
|
||||||
case check_access(Command, Access, Auth, CallerInfo) of
|
case check_access(Command, Access, Auth, CallerInfo) of
|
||||||
true ->
|
true ->
|
||||||
check_access_command(Commands, Command,
|
check_access_command(Commands, Command,
|
||||||
ArgumentRestrictions,
|
ArgumentRestrictions,
|
||||||
Method, Arguments);
|
Method, Arguments);
|
||||||
false ->
|
false ->
|
||||||
false
|
false
|
||||||
end;
|
end;
|
||||||
({Access, Commands}) ->
|
({Access, Commands}) ->
|
||||||
ArgumentRestrictions = [],
|
ArgumentRestrictions = [],
|
||||||
case check_access(Command, Access, Auth, CallerInfo) of
|
case check_access(Command, Access, Auth, CallerInfo) of
|
||||||
true ->
|
true ->
|
||||||
check_access_command(Commands, Command,
|
check_access_command(Commands, Command,
|
||||||
ArgumentRestrictions,
|
ArgumentRestrictions,
|
||||||
Method, Arguments);
|
Method, Arguments);
|
||||||
false ->
|
false ->
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
AccessCommands),
|
AccessCommands),
|
||||||
case AccessCommandsAllowed of
|
case AccessCommandsAllowed of
|
||||||
[] -> throw({error, account_unprivileged});
|
[] -> throw({error, account_unprivileged});
|
||||||
L when is_list(L) -> ok
|
L when is_list(L) -> ok
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec check_auth(ejabberd_commands(), noauth) -> noauth_provided;
|
-spec check_auth(ejabberd_commands(), noauth) -> noauth_provided;
|
||||||
@ -686,9 +729,9 @@ check_access2(Access, AccessInfo, Server) ->
|
|||||||
check_access_command(Commands, Command, ArgumentRestrictions,
|
check_access_command(Commands, Command, ArgumentRestrictions,
|
||||||
Method, Arguments) ->
|
Method, Arguments) ->
|
||||||
case Commands==all orelse lists:member(Method, Commands) of
|
case Commands==all orelse lists:member(Method, Commands) of
|
||||||
true -> check_access_arguments(Command, ArgumentRestrictions,
|
true -> check_access_arguments(Command, ArgumentRestrictions,
|
||||||
Arguments);
|
Arguments);
|
||||||
false -> false
|
false -> false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_access_arguments(Command, ArgumentRestrictions, Arguments) ->
|
check_access_arguments(Command, ArgumentRestrictions, Arguments) ->
|
||||||
@ -711,19 +754,23 @@ tag_arguments(ArgsDefs, Args) ->
|
|||||||
Args).
|
Args).
|
||||||
|
|
||||||
|
|
||||||
|
%% Get commands for all version
|
||||||
|
get_all_access_commands(AccessCommands) ->
|
||||||
|
get_access_commands(AccessCommands, ?DEFAULT_VERSION).
|
||||||
|
|
||||||
get_access_commands(undefined, Version) ->
|
get_access_commands(undefined, Version) ->
|
||||||
Cmds = get_commands(Version),
|
Cmds = get_exposed_commands(Version),
|
||||||
[{?POLICY_ACCESS, Cmds, []}];
|
[{?POLICY_ACCESS, Cmds, []}];
|
||||||
get_access_commands(AccessCommands, _Version) ->
|
get_access_commands(AccessCommands, _Version) ->
|
||||||
AccessCommands.
|
AccessCommands.
|
||||||
|
|
||||||
get_commands() ->
|
get_exposed_commands() ->
|
||||||
get_commands(?DEFAULT_VERSION).
|
get_exposed_commands(?DEFAULT_VERSION).
|
||||||
get_commands(Version) ->
|
get_exposed_commands(Version) ->
|
||||||
Opts0 = ejabberd_config:get_option(
|
Opts0 = ejabberd_config:get_option(
|
||||||
commands,
|
commands,
|
||||||
fun(V) when is_list(V) -> V end,
|
fun(V) when is_list(V) -> V end,
|
||||||
[]),
|
[]),
|
||||||
Opts = lists:map(fun(V) when is_tuple(V) -> [V]; (V) -> V end, Opts0),
|
Opts = lists:map(fun(V) when is_tuple(V) -> [V]; (V) -> V end, Opts0),
|
||||||
CommandsList = list_commands_policy(Version),
|
CommandsList = list_commands_policy(Version),
|
||||||
OpenCmds = [N || {N, _, _, open} <- CommandsList],
|
OpenCmds = [N || {N, _, _, open} <- CommandsList],
|
||||||
@ -733,27 +780,32 @@ get_commands(Version) ->
|
|||||||
Cmds =
|
Cmds =
|
||||||
lists:foldl(
|
lists:foldl(
|
||||||
fun([{add_commands, L}], Acc) ->
|
fun([{add_commands, L}], Acc) ->
|
||||||
Cmds = case L of
|
Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
|
||||||
open -> OpenCmds;
|
|
||||||
restricted -> RestrictedCmds;
|
|
||||||
admin -> AdminCmds;
|
|
||||||
user -> UserCmds;
|
|
||||||
_ when is_list(L) -> L
|
|
||||||
end,
|
|
||||||
lists:usort(Cmds ++ Acc);
|
lists:usort(Cmds ++ Acc);
|
||||||
([{remove_commands, L}], Acc) ->
|
([{remove_commands, L}], Acc) ->
|
||||||
Cmds = case L of
|
Cmds = expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds),
|
||||||
open -> OpenCmds;
|
|
||||||
restricted -> RestrictedCmds;
|
|
||||||
admin -> AdminCmds;
|
|
||||||
user -> UserCmds;
|
|
||||||
_ when is_list(L) -> L
|
|
||||||
end,
|
|
||||||
Acc -- Cmds;
|
Acc -- Cmds;
|
||||||
(_, Acc) -> Acc
|
(_, Acc) -> Acc
|
||||||
end, AdminCmds ++ UserCmds, Opts),
|
end, [], Opts),
|
||||||
Cmds.
|
Cmds.
|
||||||
|
|
||||||
|
%% This is used to allow mixing command policy (like open, user, admin, restricted), with command entry
|
||||||
|
expand_commands(L, OpenCmds, UserCmds, AdminCmds, RestrictedCmds) when is_list(L) ->
|
||||||
|
lists:foldl(fun(open, Acc) -> OpenCmds ++ Acc;
|
||||||
|
(user, Acc) -> UserCmds ++ Acc;
|
||||||
|
(admin, Acc) -> AdminCmds ++ Acc;
|
||||||
|
(restricted, Acc) -> RestrictedCmds ++ Acc;
|
||||||
|
(Command, Acc) when is_atom(Command) ->
|
||||||
|
[Command|Acc]
|
||||||
|
end, [], L).
|
||||||
|
|
||||||
|
oauth_token_user(noauth) ->
|
||||||
|
undefined;
|
||||||
|
oauth_token_user(admin) ->
|
||||||
|
undefined;
|
||||||
|
oauth_token_user({User, Server, _, _}) ->
|
||||||
|
jid:make(User, Server, <<>>).
|
||||||
|
|
||||||
is_admin(_Name, admin, _Extra) ->
|
is_admin(_Name, admin, _Extra) ->
|
||||||
true;
|
true;
|
||||||
is_admin(_Name, {_User, _Server, _, false}, _Extra) ->
|
is_admin(_Name, {_User, _Server, _, false}, _Extra) ->
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
%% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin
|
%% * Using the command line and oauth_issue_token command, the token is generated in behalf of ejabberd' sysadmin
|
||||||
%% (as it has access to ejabberd command line).
|
%% (as it has access to ejabberd command line).
|
||||||
|
|
||||||
-define(EXPIRE, 3600).
|
-define(EXPIRE, 31536000).
|
||||||
|
|
||||||
start() ->
|
start() ->
|
||||||
DBMod = get_db_backend(),
|
DBMod = get_db_backend(),
|
||||||
@ -215,7 +215,7 @@ authenticate_user({User, Server}, Ctx) ->
|
|||||||
authenticate_client(Client, Ctx) -> {ok, {Ctx, {client, Client}}}.
|
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_exposed_commands(),
|
||||||
Cmds1 = ['ejabberd:user', 'ejabberd:admin', 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),
|
||||||
@ -237,7 +237,7 @@ get_cmd_scopes() ->
|
|||||||
dict:append(Scope, Cmd, Accum2)
|
dict:append(Scope, Cmd, Accum2)
|
||||||
end, Accum, Scopes);
|
end, Accum, Scopes);
|
||||||
_ -> Accum
|
_ -> Accum
|
||||||
end end, dict:new(), ejabberd_commands:get_commands()),
|
end end, dict:new(), ejabberd_commands:get_exposed_commands()),
|
||||||
ScopeMap.
|
ScopeMap.
|
||||||
|
|
||||||
%% 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
|
||||||
@ -371,12 +371,10 @@ process(_Handlers,
|
|||||||
?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?CT(<<": ">>)]),
|
?LABEL(<<"ttl">>, [?CT(<<"Token TTL">>), ?CT(<<": ">>)]),
|
||||||
?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}],
|
?XAE(<<"select">>, [{<<"name">>, <<"ttl">>}],
|
||||||
[
|
[
|
||||||
?XAC(<<"option">>, [{<<"selected">>, <<"selected">>},
|
|
||||||
{<<"value">>, jlib:integer_to_binary(expire())}],<<"Default (", (integer_to_binary(expire()))/binary, " seconds)">>),
|
|
||||||
?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>),
|
?XAC(<<"option">>, [{<<"value">>, <<"3600">>}],<<"1 Hour">>),
|
||||||
?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>),
|
?XAC(<<"option">>, [{<<"value">>, <<"86400">>}],<<"1 Day">>),
|
||||||
?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>),
|
?XAC(<<"option">>, [{<<"value">>, <<"2592000">>}],<<"1 Month">>),
|
||||||
?XAC(<<"option">>, [{<<"value">>, <<"31536000">>}],<<"1 Year">>),
|
?XAC(<<"option">>, [{<<"selected">>, <<"selected">>},{<<"value">>, <<"31536000">>}],<<"1 Year">>),
|
||||||
?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]),
|
?XAC(<<"option">>, [{<<"value">>, <<"315360000">>}],<<"10 Years">>)]),
|
||||||
?BR,
|
?BR,
|
||||||
?INPUTT(<<"submit">>, <<"">>, <<"Accept">>)
|
?INPUTT(<<"submit">>, <<"">>, <<"Accept">>)
|
||||||
@ -482,7 +480,7 @@ process(_Handlers,
|
|||||||
process(_Handlers,
|
process(_Handlers,
|
||||||
#request{method = 'POST', q = Q, lang = _Lang,
|
#request{method = 'POST', q = Q, lang = _Lang,
|
||||||
path = [_, <<"token">>]}) ->
|
path = [_, <<"token">>]}) ->
|
||||||
case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
|
case proplists:get_value(<<"grant_type">>, Q, <<"">>) of
|
||||||
<<"password">> ->
|
<<"password">> ->
|
||||||
SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
|
SScope = proplists:get_value(<<"scope">>, Q, <<"">>),
|
||||||
StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
|
StringJID = proplists:get_value(<<"username">>, Q, <<"">>),
|
||||||
@ -518,13 +516,10 @@ process(_Handlers,
|
|||||||
{<<"scope">>, str:join(VerifiedScope, <<" ">>)},
|
{<<"scope">>, str:join(VerifiedScope, <<" ">>)},
|
||||||
{<<"expires_in">>, Expires}]});
|
{<<"expires_in">>, Expires}]});
|
||||||
{error, Error} when is_atom(Error) ->
|
{error, Error} when is_atom(Error) ->
|
||||||
json_response(400, {[
|
json_error(400, <<"invalid_grant">>, Error)
|
||||||
{<<"error">>, <<"invalid_grant">>},
|
|
||||||
{<<"error_description">>, Error}]})
|
|
||||||
end;
|
end;
|
||||||
_OtherGrantType ->
|
_OtherGrantType ->
|
||||||
json_response(400, {[
|
json_error(400, <<"unsupported_grant_type">>, unsupported_grant_type)
|
||||||
{<<"error">>, <<"unsupported_grant_type">>}]})
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
process(_Handlers, _Request) ->
|
process(_Handlers, _Request) ->
|
||||||
@ -540,14 +535,24 @@ get_db_backend() ->
|
|||||||
list_to_atom("ejabberd_oauth_" ++ atom_to_list(DBType)).
|
list_to_atom("ejabberd_oauth_" ++ atom_to_list(DBType)).
|
||||||
|
|
||||||
|
|
||||||
%% Headers as per RFC 6749
|
%% Headers as per RFC 6749
|
||||||
json_response(Code, Body) ->
|
json_response(Code, Body) ->
|
||||||
{Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>},
|
{Code, [{<<"Content-Type">>, <<"application/json;charset=UTF-8">>},
|
||||||
{<<"Cache-Control">>, <<"no-store">>},
|
{<<"Cache-Control">>, <<"no-store">>},
|
||||||
{<<"Pragma">>, <<"no-cache">>}],
|
{<<"Pragma">>, <<"no-cache">>}],
|
||||||
jiffy:encode(Body)}.
|
jiffy:encode(Body)}.
|
||||||
|
|
||||||
|
%% OAauth error are defined in:
|
||||||
|
%% https://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-5.2
|
||||||
|
json_error(Code, Error, Reason) ->
|
||||||
|
Desc = json_error_desc(Reason),
|
||||||
|
Body = {[{<<"error">>, Error},
|
||||||
|
{<<"error_description">>, Desc}]},
|
||||||
|
json_response(Code, Body).
|
||||||
|
|
||||||
|
json_error_desc(access_denied) -> <<"Access denied">>;
|
||||||
|
json_error_desc(unsupported_grant_type) -> <<"Unsupported grant type">>;
|
||||||
|
json_error_desc(invalid_scope) -> <<"Invalid scope">>.
|
||||||
|
|
||||||
web_head() ->
|
web_head() ->
|
||||||
[?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>},
|
[?XA(<<"meta">>, [{<<"http-equiv">>, <<"X-UA-Compatible">>},
|
||||||
@ -661,7 +666,7 @@ css() ->
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container > .section {
|
.container > .section {
|
||||||
background: #424A55;
|
background: #424A55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,12 +96,6 @@ get_acl_rule(_RPath, 'POST') ->
|
|||||||
access, fun(A) -> A end, configure),
|
access, fun(A) -> A end, configure),
|
||||||
{global, [AC]}.
|
{global, [AC]}.
|
||||||
|
|
||||||
is_acl_match(Host, Rules, Jid) ->
|
|
||||||
lists:any(fun (Rule) ->
|
|
||||||
allow == acl:match_rule(Host, Rule, Jid)
|
|
||||||
end,
|
|
||||||
Rules).
|
|
||||||
|
|
||||||
%%%==================================
|
%%%==================================
|
||||||
%%%% Menu Items Access
|
%%%% Menu Items Access
|
||||||
|
|
||||||
@ -151,7 +145,7 @@ is_allowed_path([<<"admin">> | Path], JID) ->
|
|||||||
is_allowed_path(Path, JID);
|
is_allowed_path(Path, JID);
|
||||||
is_allowed_path(Path, JID) ->
|
is_allowed_path(Path, JID) ->
|
||||||
{HostOfRule, AccessRule} = get_acl_rule(Path, 'GET'),
|
{HostOfRule, AccessRule} = get_acl_rule(Path, 'GET'),
|
||||||
is_acl_match(HostOfRule, AccessRule, JID).
|
acl:any_rules_allowed(HostOfRule, AccessRule, JID).
|
||||||
|
|
||||||
%% @spec(Path) -> URL
|
%% @spec(Path) -> URL
|
||||||
%% where Path = [string()]
|
%% where Path = [string()]
|
||||||
@ -279,8 +273,8 @@ get_auth_account(HostOfRule, AccessRule, User, Server,
|
|||||||
Pass) ->
|
Pass) ->
|
||||||
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
|
case ejabberd_auth:check_password(User, <<"">>, Server, Pass) of
|
||||||
true ->
|
true ->
|
||||||
case is_acl_match(HostOfRule, AccessRule,
|
case acl:any_rules_allowed(HostOfRule, AccessRule,
|
||||||
jid:make(User, Server, <<"">>))
|
jid:make(User, Server, <<"">>))
|
||||||
of
|
of
|
||||||
false -> {unauthorized, <<"unprivileged-account">>};
|
false -> {unauthorized, <<"unprivileged-account">>};
|
||||||
true -> {ok, {User, Server}}
|
true -> {ok, {User, Server}}
|
||||||
@ -1346,7 +1340,7 @@ parse_access_rule(Text) ->
|
|||||||
list_vhosts(Lang, JID) ->
|
list_vhosts(Lang, JID) ->
|
||||||
Hosts = (?MYHOSTS),
|
Hosts = (?MYHOSTS),
|
||||||
HostsAllowed = lists:filter(fun (Host) ->
|
HostsAllowed = lists:filter(fun (Host) ->
|
||||||
is_acl_match(Host,
|
acl:any_rules_allowed(Host,
|
||||||
[configure, webadmin_view],
|
[configure, webadmin_view],
|
||||||
JID)
|
JID)
|
||||||
end,
|
end,
|
||||||
|
26
src/jid.erl
26
src/jid.erl
@ -50,11 +50,35 @@
|
|||||||
-spec start() -> ok.
|
-spec start() -> ok.
|
||||||
|
|
||||||
start() ->
|
start() ->
|
||||||
|
{ok, Owner} = ets_owner(),
|
||||||
SplitPattern = binary:compile_pattern([<<"@">>, <<"/">>]),
|
SplitPattern = binary:compile_pattern([<<"@">>, <<"/">>]),
|
||||||
catch ets:new(jlib, [named_table, protected, set, {keypos, 1}]),
|
%% Table is public to allow ETS insert to fix / update the table even if table already exist
|
||||||
|
%% with another owner.
|
||||||
|
catch ets:new(jlib, [named_table, public, set, {keypos, 1}, {heir, Owner, undefined}]),
|
||||||
ets:insert(jlib, {string_to_jid_pattern, SplitPattern}),
|
ets:insert(jlib, {string_to_jid_pattern, SplitPattern}),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
ets_owner() ->
|
||||||
|
case whereis(jlib_ets) of
|
||||||
|
undefined ->
|
||||||
|
Pid = spawn(fun() -> ets_keepalive() end),
|
||||||
|
case catch register(jlib_ets, Pid) of
|
||||||
|
true ->
|
||||||
|
{ok, Pid};
|
||||||
|
Error -> Error
|
||||||
|
end;
|
||||||
|
Pid ->
|
||||||
|
{ok,Pid}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Process used to keep jlib ETS table alive in case the original owner dies.
|
||||||
|
%% The table need to be public, otherwise subsequent inserts would fail.
|
||||||
|
ets_keepalive() ->
|
||||||
|
receive
|
||||||
|
_ ->
|
||||||
|
ets_keepalive()
|
||||||
|
end.
|
||||||
|
|
||||||
-spec make(binary(), binary(), binary()) -> jid() | error.
|
-spec make(binary(), binary(), binary()) -> jid() | error.
|
||||||
|
|
||||||
make(User, Server, Resource) ->
|
make(User, Server, Resource) ->
|
||||||
|
@ -136,7 +136,7 @@ check_permissions(Request, Command) ->
|
|||||||
{ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call),
|
{ok, CommandPolicy, Scope} = ejabberd_commands:get_command_policy_and_scope(Call),
|
||||||
check_permissions2(Request, Call, CommandPolicy, Scope);
|
check_permissions2(Request, Call, CommandPolicy, Scope);
|
||||||
_ ->
|
_ ->
|
||||||
unauthorized_response()
|
json_error(404, 40, <<"Endpoint not found.">>)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList)
|
check_permissions2(#request{auth = HTTPAuth, headers = Headers}, Call, _, ScopeList)
|
||||||
@ -220,8 +220,12 @@ process([Call], #request{method = 'POST', data = Data, ip = {IP, _} = IPPort} =
|
|||||||
log(Call, Args, IPPort),
|
log(Call, Args, IPPort),
|
||||||
case check_permissions(Req, Call) of
|
case check_permissions(Req, Call) of
|
||||||
{allowed, Cmd, Auth} ->
|
{allowed, Cmd, Auth} ->
|
||||||
{Code, Result} = handle(Cmd, Auth, Args, Version, IP),
|
case handle(Cmd, Auth, Args, Version, IP) of
|
||||||
json_response(Code, jiffy:encode(Result));
|
{Code, Result} ->
|
||||||
|
json_response(Code, jiffy:encode(Result));
|
||||||
|
{HTMLCode, JSONErrorCode, Message} ->
|
||||||
|
json_error(HTMLCode, JSONErrorCode, Message)
|
||||||
|
end;
|
||||||
%% Warning: check_permission direcly formats 401 reply if not authorized
|
%% Warning: check_permission direcly formats 401 reply if not authorized
|
||||||
ErrorResponse ->
|
ErrorResponse ->
|
||||||
ErrorResponse
|
ErrorResponse
|
||||||
@ -264,10 +268,10 @@ get_api_version(#request{path = Path}) ->
|
|||||||
get_api_version(lists:reverse(Path));
|
get_api_version(lists:reverse(Path));
|
||||||
get_api_version([<<"v", String/binary>> | Tail]) ->
|
get_api_version([<<"v", String/binary>> | Tail]) ->
|
||||||
case catch jlib:binary_to_integer(String) of
|
case catch jlib:binary_to_integer(String) of
|
||||||
N when is_integer(N) ->
|
N when is_integer(N) ->
|
||||||
N;
|
N;
|
||||||
_ ->
|
_ ->
|
||||||
get_api_version(Tail)
|
get_api_version(Tail)
|
||||||
end;
|
end;
|
||||||
get_api_version([_Head | Tail]) ->
|
get_api_version([_Head | Tail]) ->
|
||||||
get_api_version(Tail);
|
get_api_version(Tail);
|
||||||
@ -278,6 +282,8 @@ get_api_version([]) ->
|
|||||||
%% command handlers
|
%% command handlers
|
||||||
%% ----------------
|
%% ----------------
|
||||||
|
|
||||||
|
%% TODO Check accept types of request before decided format of reply.
|
||||||
|
|
||||||
% generic ejabberd command handler
|
% generic ejabberd command handler
|
||||||
handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
|
handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
|
||||||
case ejabberd_commands:get_command_format(Call, Auth, Version) of
|
case ejabberd_commands:get_command_format(Call, Auth, Version) of
|
||||||
@ -309,8 +315,10 @@ handle(Call, Auth, Args, Version, IP) when is_atom(Call), is_list(Args) ->
|
|||||||
{401, jlib:atom_to_binary(Why)};
|
{401, jlib:atom_to_binary(Why)};
|
||||||
throw:{not_allowed, Msg} ->
|
throw:{not_allowed, Msg} ->
|
||||||
{401, iolist_to_binary(Msg)};
|
{401, iolist_to_binary(Msg)};
|
||||||
throw:{error, account_unprivileged} ->
|
throw:{error, account_unprivileged} ->
|
||||||
{401, iolist_to_binary(<<"Unauthorized: Account Unpriviledged">>)};
|
{403, 31, <<"Command need to be run with admin priviledge.">>};
|
||||||
|
throw:{error, access_rules_unauthorized} ->
|
||||||
|
{403, 32, <<"AccessRules: Account associated to token does not have the right to perform the operation.">>};
|
||||||
throw:{invalid_parameter, Msg} ->
|
throw:{invalid_parameter, Msg} ->
|
||||||
{400, iolist_to_binary(Msg)};
|
{400, iolist_to_binary(Msg)};
|
||||||
throw:{error, Why} when is_atom(Why) ->
|
throw:{error, Why} when is_atom(Why) ->
|
||||||
@ -490,9 +498,7 @@ format_result(404, {_Name, _}) ->
|
|||||||
"not_found".
|
"not_found".
|
||||||
|
|
||||||
unauthorized_response() ->
|
unauthorized_response() ->
|
||||||
unauthorized_response(<<"401 Unauthorized">>).
|
json_error(401, 10, <<"Oauth Token is invalid or expired.">>).
|
||||||
unauthorized_response(Body) ->
|
|
||||||
json_response(401, jiffy:encode(Body)).
|
|
||||||
|
|
||||||
badrequest_response() ->
|
badrequest_response() ->
|
||||||
badrequest_response(<<"400 Bad Request">>).
|
badrequest_response(<<"400 Bad Request">>).
|
||||||
@ -502,6 +508,15 @@ badrequest_response(Body) ->
|
|||||||
json_response(Code, Body) when is_integer(Code) ->
|
json_response(Code, Body) when is_integer(Code) ->
|
||||||
{Code, ?HEADER(?CT_JSON), Body}.
|
{Code, ?HEADER(?CT_JSON), Body}.
|
||||||
|
|
||||||
|
%% HTTPCode, JSONCode = integers
|
||||||
|
%% message is binary
|
||||||
|
json_error(HTTPCode, JSONCode, Message) ->
|
||||||
|
{HTTPCode, ?HEADER(?CT_JSON),
|
||||||
|
jiffy:encode({[{<<"status">>, <<"error">>},
|
||||||
|
{<<"code">>, JSONCode},
|
||||||
|
{<<"message">>, Message}]})
|
||||||
|
}.
|
||||||
|
|
||||||
log(Call, Args, {Addr, Port}) ->
|
log(Call, Args, {Addr, Port}) ->
|
||||||
AddrS = jlib:ip_to_list({Addr, Port}),
|
AddrS = jlib:ip_to_list({Addr, Port}),
|
||||||
?INFO_MSG("API call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]);
|
?INFO_MSG("API call ~s ~p from ~s:~p", [Call, Args, AddrS, Port]);
|
||||||
|
@ -18,9 +18,13 @@
|
|||||||
#
|
#
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
## TODO Fix next test error: add admin user ACL
|
||||||
|
|
||||||
defmodule EjabberdCommandsMockTest do
|
defmodule EjabberdCommandsMockTest do
|
||||||
use ExUnit.Case, async: false
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
require EjabberdOauthMock
|
||||||
|
|
||||||
@author "jsautret@process-one.net"
|
@author "jsautret@process-one.net"
|
||||||
|
|
||||||
# mocked callback module
|
# mocked callback module
|
||||||
@ -44,8 +48,11 @@ defmodule EjabberdCommandsMockTest do
|
|||||||
_ -> :ok
|
_ -> :ok
|
||||||
end
|
end
|
||||||
:mnesia.start
|
:mnesia.start
|
||||||
|
:ok = :jid.start
|
||||||
|
:ok = :ejabberd_config.start(["domain1", "domain2"], [])
|
||||||
|
:ok = :acl.start
|
||||||
EjabberdOauthMock.init
|
EjabberdOauthMock.init
|
||||||
:ok
|
on_exit fn -> :meck.unload end
|
||||||
end
|
end
|
||||||
|
|
||||||
setup do
|
setup do
|
||||||
@ -180,7 +187,7 @@ defmodule EjabberdCommandsMockTest do
|
|||||||
|
|
||||||
|
|
||||||
test "API command with user policy" do
|
test "API command with user policy" do
|
||||||
mock_commands_config
|
mock_commands_config [:user, :admin]
|
||||||
|
|
||||||
# Register a command test(user, domain) -> {:versionN, user, domain}
|
# Register a command test(user, domain) -> {:versionN, user, domain}
|
||||||
# with policy=user and versions 1 & 3
|
# with policy=user and versions 1 & 3
|
||||||
@ -313,9 +320,8 @@ defmodule EjabberdCommandsMockTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
test "API command with admin policy" do
|
test "API command with admin policy" do
|
||||||
mock_commands_config
|
mock_commands_config [:admin]
|
||||||
|
|
||||||
# Register a command test(user, domain) -> {user, domain}
|
# Register a command test(user, domain) -> {user, domain}
|
||||||
# with policy=admin
|
# with policy=admin
|
||||||
@ -393,13 +399,47 @@ defmodule EjabberdCommandsMockTest do
|
|||||||
assert :meck.validate @module
|
assert :meck.validate @module
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "Commands can perform extra check on access" do
|
||||||
|
mock_commands_config [:admin, :open]
|
||||||
|
|
||||||
|
command_name = :test
|
||||||
|
function = :test_command
|
||||||
|
command = ejabberd_commands(name: command_name,
|
||||||
|
args: [{:user, :binary}, {:host, :binary}],
|
||||||
|
access: [:basic_rule_1],
|
||||||
|
module: @module,
|
||||||
|
function: function,
|
||||||
|
policy: :open)
|
||||||
|
:meck.expect(@module, function,
|
||||||
|
fn(user, domain) when is_binary(user) and is_binary(domain) ->
|
||||||
|
{user, domain}
|
||||||
|
end)
|
||||||
|
assert :ok == :ejabberd_commands.register_commands [command]
|
||||||
|
|
||||||
|
# :acl.add(:global, :basic_acl_1, {:user, @user, @host})
|
||||||
|
# :acl.add_access(:global, :basic_rule_1, [{:allow, [{:acl, :basic_acl_1}]}])
|
||||||
|
|
||||||
|
assert {@user, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@user, @domain,
|
||||||
|
@userpass, false},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain])
|
||||||
|
assert {@user, @domain} ==
|
||||||
|
:ejabberd_commands.execute_command(:undefined,
|
||||||
|
{@admin, @domain,
|
||||||
|
@adminpass, false},
|
||||||
|
command_name,
|
||||||
|
[@user, @domain])
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
##########################################################
|
##########################################################
|
||||||
# Utils
|
# Utils
|
||||||
|
|
||||||
# Mock a config where only @admin user is allowed to call commands
|
# Mock a config where only @admin user is allowed to call commands
|
||||||
# as admin
|
# as admin
|
||||||
def mock_commands_config do
|
def mock_commands_config(commands \\ []) do
|
||||||
EjabberdAuthMock.init
|
EjabberdAuthMock.init
|
||||||
EjabberdAuthMock.create_user @user, @domain, @userpass
|
EjabberdAuthMock.create_user @user, @domain, @userpass
|
||||||
EjabberdAuthMock.create_user @admin, @domain, @adminpass
|
EjabberdAuthMock.create_user @admin, @domain, @adminpass
|
||||||
@ -408,10 +448,12 @@ defmodule EjabberdCommandsMockTest do
|
|||||||
:meck.expect(:ejabberd_config, :get_option,
|
:meck.expect(:ejabberd_config, :get_option,
|
||||||
fn(:commands_admin_access, _, _) -> :commands_admin_access
|
fn(:commands_admin_access, _, _) -> :commands_admin_access
|
||||||
(:oauth_access, _, _) -> :all
|
(:oauth_access, _, _) -> :all
|
||||||
|
(:commands, _, _) -> [{:add_commands, commands}]
|
||||||
(_, _, default) -> default
|
(_, _, default) -> default
|
||||||
end)
|
end)
|
||||||
:meck.expect(:ejabberd_config, :get_myhosts,
|
:meck.expect(:ejabberd_config, :get_myhosts,
|
||||||
fn() -> [@domain] end)
|
fn() -> [@domain] end)
|
||||||
|
|
||||||
:meck.new :acl
|
:meck.new :acl
|
||||||
:meck.expect(:acl, :access_matches,
|
:meck.expect(:acl, :access_matches,
|
||||||
fn(:commands_admin_access, info, _scope) ->
|
fn(:commands_admin_access, info, _scope) ->
|
||||||
|
@ -28,7 +28,11 @@ defmodule EjabberdCommandsTest do
|
|||||||
|
|
||||||
setup_all do
|
setup_all do
|
||||||
:mnesia.start
|
:mnesia.start
|
||||||
|
:stringprep.start
|
||||||
|
:ok = :ejabberd_config.start(["localhost"], [])
|
||||||
|
|
||||||
:ejabberd_commands.init
|
:ejabberd_commands.init
|
||||||
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Check that we can register a command" do
|
test "Check that we can register a command" do
|
||||||
@ -37,6 +41,14 @@ defmodule EjabberdCommandsTest do
|
|||||||
assert Enum.member?(commands, {:test_user, [], "Test user"})
|
assert Enum.member?(commands, {:test_user, [], "Test user"})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "get_exposed_commands/0 returns registered commands" do
|
||||||
|
commands = [open_test_command]
|
||||||
|
:ok = :ejabberd_commands.register_commands(commands)
|
||||||
|
:ok = :ejabberd_commands.expose_commands(commands)
|
||||||
|
exposed_commands = :ejabberd_commands.get_exposed_commands
|
||||||
|
assert Enum.member?(exposed_commands, :test_open)
|
||||||
|
end
|
||||||
|
|
||||||
test "Check that admin commands are rejected with noauth credentials" do
|
test "Check that admin commands are rejected with noauth credentials" do
|
||||||
:ok = :ejabberd_commands.register_commands([admin_test_command])
|
:ok = :ejabberd_commands.register_commands([admin_test_command])
|
||||||
|
|
||||||
@ -70,6 +82,16 @@ defmodule EjabberdCommandsTest do
|
|||||||
]}}}})
|
]}}}})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp open_test_command do
|
||||||
|
ejabberd_commands(name: :test_open, tags: [:test],
|
||||||
|
desc: "Test open",
|
||||||
|
policy: :open,
|
||||||
|
module: __MODULE__,
|
||||||
|
function: :test_open,
|
||||||
|
args: [],
|
||||||
|
result: {:res, :rescode})
|
||||||
|
end
|
||||||
|
|
||||||
defp admin_test_command do
|
defp admin_test_command do
|
||||||
ejabberd_commands(name: :test_admin, tags: [:roster],
|
ejabberd_commands(name: :test_admin, tags: [:roster],
|
||||||
desc: "Test admin",
|
desc: "Test admin",
|
||||||
|
@ -71,8 +71,8 @@ defmodule EjabberdCyrsaslTest do
|
|||||||
response = "username=\"#{user}\",realm=\"#{domain}\",nonce=\"#{nonce}\",cnonce=\"#{cnonce}\"," <>
|
response = "username=\"#{user}\",realm=\"#{domain}\",nonce=\"#{nonce}\",cnonce=\"#{cnonce}\"," <>
|
||||||
"nc=\"#{nc}\",qop=auth,digest-uri=\"#{digest_uri}\",response=\"#{response_hash}\"," <>
|
"nc=\"#{nc}\",qop=auth,digest-uri=\"#{digest_uri}\",response=\"#{response_hash}\"," <>
|
||||||
"charset=utf-8,algorithm=md5-sess"
|
"charset=utf-8,algorithm=md5-sess"
|
||||||
assert {:continue, calc_str, state3} = :cyrsasl.server_step(state1, response)
|
assert {:continue, _calc_str, state3} = :cyrsasl.server_step(state1, response)
|
||||||
assert {:ok, list} = :cyrsasl.server_step(state3, "")
|
assert {:ok, _list} = :cyrsasl.server_step(state3, "")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp calc_digest_sha(user, domain, pass, nc, nonce, cnonce) do
|
defp calc_digest_sha(user, domain, pass, nc, nonce, cnonce) do
|
||||||
@ -94,7 +94,7 @@ defmodule EjabberdCyrsaslTest do
|
|||||||
defp setup_anonymous_mocks() do
|
defp setup_anonymous_mocks() do
|
||||||
:meck.unload
|
:meck.unload
|
||||||
mock(:ejabberd_auth_anonymous, :is_sasl_anonymous_enabled,
|
mock(:ejabberd_auth_anonymous, :is_sasl_anonymous_enabled,
|
||||||
fn (host) ->
|
fn (_host) ->
|
||||||
true
|
true
|
||||||
end)
|
end)
|
||||||
mock(:ejabberd_auth, :is_user_exists,
|
mock(:ejabberd_auth, :is_user_exists,
|
||||||
@ -119,7 +119,7 @@ defmodule EjabberdCyrsaslTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp check_password(user, authzid, pass) do
|
defp check_password(_user, authzid, pass) do
|
||||||
case get_password(authzid) do
|
case get_password(authzid) do
|
||||||
{^pass, mod} ->
|
{^pass, mod} ->
|
||||||
{true, mod}
|
{true, mod}
|
||||||
@ -128,7 +128,7 @@ defmodule EjabberdCyrsaslTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp check_password_digest(user, authzid, pass, digest, digest_gen) do
|
defp check_password_digest(_user, authzid, _pass, digest, digest_gen) do
|
||||||
case get_password(authzid) do
|
case get_password(authzid) do
|
||||||
{spass, mod} ->
|
{spass, mod} ->
|
||||||
v = digest_gen.(spass)
|
v = digest_gen.(spass)
|
||||||
|
@ -22,6 +22,9 @@ defmodule EjabberdModAdminExtraTest do
|
|||||||
use ExUnit.Case, async: false
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
require EjabberdAuthMock
|
require EjabberdAuthMock
|
||||||
|
require EjabberdSmMock
|
||||||
|
require ModLastMock
|
||||||
|
require ModRosterMock
|
||||||
|
|
||||||
@author "jsautret@process-one.net"
|
@author "jsautret@process-one.net"
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ defmodule ModHttpApiMockTest do
|
|||||||
end)
|
end)
|
||||||
:meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
|
:meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
|
||||||
fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8)]} end)
|
fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8)]} end)
|
||||||
:meck.expect(:ejabberd_commands, :get_commands,
|
:meck.expect(:ejabberd_commands, :get_exposed_commands,
|
||||||
fn () -> [@acommand] end)
|
fn () -> [@acommand] end)
|
||||||
:meck.expect(:ejabberd_commands, :execute_command,
|
:meck.expect(:ejabberd_commands, :execute_command,
|
||||||
fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version, _) ->
|
fn (:undefined, {@user, @domain, @userpass, false}, @acommand, [], @version, _) ->
|
||||||
@ -126,7 +126,7 @@ defmodule ModHttpApiMockTest do
|
|||||||
end)
|
end)
|
||||||
:meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
|
:meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
|
||||||
fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd: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_exposed_commands,
|
||||||
fn () -> [@acommand] end)
|
fn () -> [@acommand] end)
|
||||||
:meck.expect(:ejabberd_commands, :execute_command,
|
:meck.expect(:ejabberd_commands, :execute_command,
|
||||||
fn (:undefined, {@user, @domain, {:oauth, _token}, false},
|
fn (:undefined, {@user, @domain, {:oauth, _token}, false},
|
||||||
@ -219,7 +219,7 @@ defmodule ModHttpApiMockTest do
|
|||||||
end)
|
end)
|
||||||
:meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
|
:meck.expect(:ejabberd_commands, :get_command_policy_and_scope,
|
||||||
fn (@acommand) -> {:ok, :user, [:erlang.atom_to_binary(@acommand,:utf8), "ejabberd: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_exposed_commands,
|
||||||
fn () -> [@acommand] end)
|
fn () -> [@acommand] end)
|
||||||
:meck.expect(:ejabberd_commands, :execute_command,
|
:meck.expect(:ejabberd_commands, :execute_command,
|
||||||
fn (:undefined, {@user, @domain, {:oauth, _token}, false},
|
fn (:undefined, {@user, @domain, {:oauth, _token}, false},
|
||||||
|
@ -31,43 +31,43 @@ defmodule ModHttpApiTest do
|
|||||||
:ok = :mnesia.start
|
:ok = :mnesia.start
|
||||||
:stringprep.start
|
:stringprep.start
|
||||||
:ok = :ejabberd_config.start(["localhost"], [])
|
:ok = :ejabberd_config.start(["localhost"], [])
|
||||||
|
|
||||||
:ok = :ejabberd_commands.init
|
:ok = :ejabberd_commands.init
|
||||||
|
|
||||||
:ok = :ejabberd_commands.register_commands(cmds)
|
:ok = :ejabberd_commands.register_commands(cmds)
|
||||||
on_exit fn -> unregister_commands(cmds) end
|
on_exit fn ->
|
||||||
|
:meck.unload
|
||||||
|
unregister_commands(cmds) end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "We can expose several commands to API at a time" do
|
test "We can expose several commands to API at a time" do
|
||||||
setup_mocks()
|
setup_mocks()
|
||||||
:ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd, :user_cmd]}]])
|
:ejabberd_commands.expose_commands([:open_cmd, :user_cmd])
|
||||||
commands = :ejabberd_commands.get_commands()
|
commands = :ejabberd_commands.get_exposed_commands()
|
||||||
assert Enum.member?(commands, :open_cmd)
|
assert Enum.member?(commands, :open_cmd)
|
||||||
assert Enum.member?(commands, :user_cmd)
|
assert Enum.member?(commands, :user_cmd)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "We can call open commands without authentication" do
|
test "We can call open commands without authentication" do
|
||||||
setup_mocks()
|
setup_mocks()
|
||||||
:ejabberd_config.add_local_option(:commands, [[{:add_commands, [:open_cmd]}]])
|
:ejabberd_commands.expose_commands([:open_cmd])
|
||||||
request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
|
request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
|
||||||
{200, _, _} = :mod_http_api.process(["open_cmd"], request)
|
{200, _, _} = :mod_http_api.process(["open_cmd"], request)
|
||||||
end
|
end
|
||||||
|
|
||||||
# This related to the commands config file option
|
# This related to the commands config file option
|
||||||
test "Attempting to access a command that is not exposed as HTTP API returns 401" do
|
test "Attempting to access a command that is not exposed as HTTP API returns 403" do
|
||||||
setup_mocks()
|
setup_mocks()
|
||||||
:ejabberd_config.add_local_option(:commands, [])
|
:ejabberd_commands.expose_commands([])
|
||||||
request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
|
request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
|
||||||
{401, _, _} = :mod_http_api.process(["open_cmd"], request)
|
{403, _, _} = :mod_http_api.process(["open_cmd"], request)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Call to user, admin or restricted commands without authentication are rejected" do
|
test "Call to user, admin or restricted commands without authentication are rejected" do
|
||||||
setup_mocks()
|
setup_mocks()
|
||||||
:ejabberd_config.add_local_option(:commands, [[{:add_commands, [:user_cmd, :admin_cmd, :restricted]}]])
|
:ejabberd_commands.expose_commands([:user_cmd, :admin_cmd, :restricted])
|
||||||
request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
|
request = request(method: :POST, ip: {{127,0,0,1},50000}, data: "[]")
|
||||||
{401, _, _} = :mod_http_api.process(["user_cmd"], request)
|
{403, _, _} = :mod_http_api.process(["user_cmd"], request)
|
||||||
{401, _, _} = :mod_http_api.process(["admin_cmd"], request)
|
{403, _, _} = :mod_http_api.process(["admin_cmd"], request)
|
||||||
{401, _, _} = :mod_http_api.process(["restricted_cmd"], request)
|
{403, _, _} = :mod_http_api.process(["restricted_cmd"], request)
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag pending: true
|
@tag pending: true
|
||||||
@ -98,7 +98,7 @@ defmodule ModHttpApiTest do
|
|||||||
defp setup_mocks() do
|
defp setup_mocks() do
|
||||||
:meck.unload
|
:meck.unload
|
||||||
mock(:gen_mod, :get_module_opt,
|
mock(:gen_mod, :get_module_opt,
|
||||||
fn (_server, :mod_http_api, admin_ip_access, _, _) ->
|
fn (_server, :mod_http_api, _admin_ip_access, _, _) ->
|
||||||
[{:allow, [{:ip, {{127,0,0,2}, 32}}]}]
|
[{:allow, [{:ip, {{127,0,0,2}, 32}}]}]
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
7
test/test_helper.exs
Normal file
7
test/test_helper.exs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Code.require_file "ejabberd_auth_mock.exs", __DIR__
|
||||||
|
Code.require_file "ejabberd_oauth_mock.exs", __DIR__
|
||||||
|
Code.require_file "ejabberd_sm_mock.exs", __DIR__
|
||||||
|
Code.require_file "mod_last_mock.exs", __DIR__
|
||||||
|
Code.require_file "mod_roster_mock.exs", __DIR__
|
||||||
|
|
||||||
|
ExUnit.start
|
Loading…
Reference in New Issue
Block a user