Keep learning, keep living...

0%

ejabberd中ACL实现

ejbberd中多个模块或组件都允许管理员在配置文件中基于用户JID进行访问限制, 如在ejabberd_c2s模块中禁用某用户.

ejabberd实现了一套通用的ACL机制来满足各模块的需求.

配置方法如下:

  • 添加acl规则, 如:

    1
    {acl, blocked, {user, "test"}}.

    acl元组第2个元素为该条acl规则的名称, 第3个元素为JID的过滤规则. 示例中的过滤规则表示用户JID的User部分为”test”.

  • 添加access规则, 如:

    1
    {access, c2s, [{deny, blocked}, {allow, all}]}.

    access元组第2个元素为access规则的名称, 第3个元素中的每个元素为一个指定值和一个acl规则名字.
    当JID满足某条acl规则时, 该条access规则的值则为该acl规则的对应值. 示例中, 当用户JID中的User部分为”test”时, access规则c2s值为deny, 否则为allow.

  • 为模块或组件指定access规则(不同的模块或组件所用的配置指令可能不同) 如:

    1
    2
    3
    4
    5
    {5222, ejabberd_c2s, [
    {access, c2s},
    {shaper, c2s_shaper},
    {max_stanza_size, 65536}
    ]}.

    按示例配置后, ejabberd_c2s会根据access规则c2s的值禁用相应用户, 如JID中User为”test”的用户全部被禁用.

下边来看具体实现:

ejabberd启动时, ejabberd_app:start/2会调用acl:start/0.

1
2
3
4
5
6
7
start() ->
mnesia:create_table(acl,
[{disc_copies, [node()]},
{type, bag},
{attributes, record_info(fields, acl)}]),
mnesia:add_table_copy(acl, node(), ram_copies),
ok.

start函数创建了一个名为acl的表,来存储各条acl规则, {type, bag}使表中可以保存多个KEY相同的record, 即可以保存多个acl名称相同的acl规则, acl的record定义为:

1
-record(acl, {aclname, aclspec}).

如配置中可以添加多条相同名字的ACL规则:

1
2
3
{acl, admin, {user, "aleksey", "localhost"}}.
{acl, admin, {user, "ermine", "example.org"}}.
{acl, admin, {user, "admin", "localhost"}}.

ejabberd启动加载配置文件时, ejabberd_config模块会将相应{acl, ...}配置写入acl表中, {access, ...}配置写入config表中, 具体过程请参考<ejabberd配置模块分析>一文.

当各模块或组件需要基于JID对访问进行限制时, 使用acl:match_rule/3来获取所配置的access规则的值, 进而根据所得的access规则的值进行后续逻辑处理. 如ejabberd_c2s模块完成XMPP的BIND流程后, 为用户建立session前, 调用match_rule/3来获取指定的access规则的值, 如果值为allow则允许请用户请求继续,否则返回禁止信息.

1
2
3
4
5
6
7
case acl:match_rule(StateData#state.server,
StateData#state.access, JID) of
allow ->
...
_ ->
...
end.

来看acl:match_rule/3实现:

1
2
3
4
5
6
7
8
9
10
11
12
match_rule(global, Rule, JID) ->
case Rule of
all -> allow;
none -> deny;
_ ->
case ejabberd_config:get_global_option({access, Rule, global}) of
undefined ->
deny;
GACLs ->
match_acls(GACLs, JID, global)
end
end;

allnone为两个特殊的access规则, 对应的值分别为allowdeny. 若规则不为这两个, 则调用
ejabberd_config:get_global_option/1来获取该条access规则所指定的一系列acl规则.
如果没有则返回deny, 否则接着调用match_acls.

1
2
3
4
5
6
7
8
9
match_acls([], _, _Host) ->
deny;
match_acls([{Access, ACL} | ACLs], JID, Host) ->
case match_acl(ACL, JID, Host) of
true ->
Access;
_ ->
match_acls(ACLs, JID, Host)
end.

match_acls对ACL列表进行递归处理, 对每条acl规则调用match_acl. 如果match_acl返回true, 则递归结束,返回该acl规则所对应的值, 如果没有任何一条acl规则满足, 则返回deny.
来看match_acl实现, 简化后的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
match_acl(ACL, JID, Host) ->
case ACL of
all -> true;
none -> false;
_ ->
{User, Server, Resource} = jlib:jid_tolower(JID),
lists:any(fun(#acl{aclspec = Spec}) ->
case Spec of
all ->
true;
{user, U} ->
(U == User)
andalso
((Host == Server) orelse
((Host == global) andalso
lists:member(Server, ?MYHOSTS)));
...

WrongSpec ->
?ERROR_MSG(
"Wrong ACL expression: ~p~n"
"Check your config file and reload it with the override_acls option enabled",
[WrongSpec]),
false
end
end,
ets:lookup(acl, {ACL, global}) ++
ets:lookup(acl, {ACL, Host}))
end.

match_aclacl表中查出该名称所有的acl规则(可能有多个), 只要其中一条acl规则匹配了用户的JID, match_acl就返回true.

具体的acl过滤条件有多个, 如

  • user
  • server
  • user_glob
  • user_regexp
    等, 具体涵义请参考acl.erl源码.

注意: access规则的值可以为任意值,不一定要是allow或者deny, 这些由特定的模块自行处理.

1
{access, max_user_sessions, [{10, all}]}.

ejabberd中acl功能实现的简单却通用, 类似逻辑可以借鉴到我们的其他项目中.