fix(authz_mnesia): add a soft limit in the API for the length of ACL rules

This commit is contained in:
firest 2024-06-06 16:22:53 +08:00
parent 07a3fbaf1a
commit 5532c7b0a6
4 changed files with 121 additions and 26 deletions

View File

@ -469,8 +469,8 @@ users(get, #{query_string := QueryString}) ->
{200, Result} {200, Result}
end; end;
users(post, #{body := Body}) when is_list(Body) -> users(post, #{body := Body}) when is_list(Body) ->
case ensure_all_not_exists(<<"username">>, username, Body) of case ensure_rules_is_valid(<<"username">>, username, Body) of
[] -> ok ->
lists:foreach( lists:foreach(
fun(#{<<"username">> := Username, <<"rules">> := Rules}) -> fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
emqx_authz_mnesia:store_rules({username, Username}, Rules) emqx_authz_mnesia:store_rules({username, Username}, Rules)
@ -478,10 +478,16 @@ users(post, #{body := Body}) when is_list(Body) ->
Body Body
), ),
{204}; {204};
Exists -> {error, rules_too_long} ->
{400, #{
code => <<"BAD_REQUEST">>,
message =>
<<"The length of rules exceeds the maximum limit.">>
}};
{error, {already_exists, Exists}} ->
{409, #{ {409, #{
code => <<"ALREADY_EXISTS">>, code => <<"ALREADY_EXISTS">>,
message => binfmt("Users '~ts' already exist", [binjoin(Exists)]) message => binfmt("User '~ts' already exist", [binjoin(Exists)])
}} }}
end. end.
@ -507,8 +513,8 @@ clients(get, #{query_string := QueryString}) ->
{200, Result} {200, Result}
end; end;
clients(post, #{body := Body}) when is_list(Body) -> clients(post, #{body := Body}) when is_list(Body) ->
case ensure_all_not_exists(<<"clientid">>, clientid, Body) of case ensure_rules_is_valid(<<"clientid">>, clientid, Body) of
[] -> ok ->
lists:foreach( lists:foreach(
fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) -> fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) ->
emqx_authz_mnesia:store_rules({clientid, ClientID}, Rules) emqx_authz_mnesia:store_rules({clientid, ClientID}, Rules)
@ -516,10 +522,16 @@ clients(post, #{body := Body}) when is_list(Body) ->
Body Body
), ),
{204}; {204};
Exists -> {error, rules_too_long} ->
{400, #{
code => <<"BAD_REQUEST">>,
message =>
<<"The length of rules exceeds the maximum limit.">>
}};
{error, {already_exists, Exists}} ->
{409, #{ {409, #{
code => <<"ALREADY_EXISTS">>, code => <<"ALREADY_EXISTS">>,
message => binfmt("Clients '~ts' already exist", [binjoin(Exists)]) message => binfmt("Client '~ts' already exist", [binjoin(Exists)])
}} }}
end. end.
@ -583,8 +595,17 @@ all(get, _) ->
}} }}
end; end;
all(post, #{body := #{<<"rules">> := Rules}}) -> all(post, #{body := #{<<"rules">> := Rules}}) ->
emqx_authz_mnesia:store_rules(all, Rules), case ensure_rules_len(Rules) of
{204}; ok ->
emqx_authz_mnesia:store_rules(all, Rules),
{204};
_ ->
{400, #{
code => <<"BAD_REQUEST">>,
message =>
<<"The length of rules exceeds the maximum limit.">>
}}
end;
all(delete, _) -> all(delete, _) ->
emqx_authz_mnesia:store_rules(all, []), emqx_authz_mnesia:store_rules(all, []),
{204}. {204}.
@ -700,24 +721,53 @@ rules_example({ExampleName, ExampleType}) ->
} }
}. }.
ensure_all_not_exists(Key, Type, Cfgs) -> ensure_rules_len(Rules) ->
lists:foldl( emqx_authz_api_sources:with_source(
fun(#{Key := Id}, Acc) -> ?AUTHZ_TYPE_BIN,
case emqx_authz_mnesia:get_rules({Type, Id}) of fun(#{<<"max_rules_len">> := MaxLen}) ->
not_found -> ensure_rules_len(Rules, MaxLen)
Acc; end
_ ->
[Id | Acc]
end
end,
[],
Cfgs
). ).
ensure_rules_len(Rules, MaxLen) ->
case erlang:length(Rules) =< MaxLen of
true ->
ok;
_ ->
{error, rules_too_long}
end.
ensure_rules_is_valid(Key, Type, Cfgs) ->
MaxLen = emqx_authz_api_sources:with_source(
?AUTHZ_TYPE_BIN,
fun(#{<<"max_rules_len">> := MaxLen}) ->
MaxLen
end
),
ensure_rules_is_valid(Key, Type, MaxLen, Cfgs).
ensure_rules_is_valid(Key, Type, MaxLen, [Cfg | Cfgs]) ->
#{Key := Id, <<"rules">> := Rules} = Cfg,
case emqx_authz_mnesia:get_rules({Type, Id}) of
not_found ->
case ensure_rules_len(Rules, MaxLen) of
ok ->
ensure_rules_is_valid(Key, Type, MaxLen, Cfgs);
Error ->
Error
end;
_ ->
{error, {already_exists, Id}}
end;
ensure_rules_is_valid(_Key, _Type, _MaxLen, []) ->
ok.
binjoin([Bin]) -> binjoin([Bin]) ->
Bin; Bin;
binjoin(Bins) -> binjoin(Bins) when is_list(Bins) ->
binjoin(Bins, <<>>). binjoin(Bins, <<>>);
binjoin(Bin) ->
Bin.
binjoin([H | T], Acc) -> binjoin([H | T], Acc) ->
binjoin(T, <<H/binary, $,, Acc/binary>>); binjoin(T, <<H/binary, $,, Acc/binary>>);

View File

@ -30,12 +30,24 @@
namespace/0 namespace/0
]). ]).
-define(MAX_RULES_LEN, 100).
namespace() -> "authz". namespace() -> "authz".
type() -> ?AUTHZ_TYPE. type() -> ?AUTHZ_TYPE.
fields(builtin_db) -> fields(builtin_db) ->
emqx_authz_schema:authz_common_fields(?AUTHZ_TYPE). emqx_authz_schema:authz_common_fields(?AUTHZ_TYPE) ++
[
{max_rules_len,
?HOCON(
pos_integer(),
#{
default => ?MAX_RULES_LEN,
desc => ?DESC(max_rules_len)
}
)}
].
source_refs() -> source_refs() ->
[?R_REF(builtin_db)]. [?R_REF(builtin_db)].

View File

@ -36,7 +36,7 @@ init_per_suite(Config) ->
{emqx_conf, {emqx_conf,
"authorization.cache { enable = false }," "authorization.cache { enable = false },"
"authorization.no_match = deny," "authorization.no_match = deny,"
"authorization.sources = [{type = built_in_database}]"}, "authorization.sources = [{type = built_in_database, max_rules_len = 5}]"},
emqx, emqx,
emqx_auth, emqx_auth,
emqx_auth_mnesia, emqx_auth_mnesia,
@ -66,6 +66,14 @@ t_api(_) ->
[?USERNAME_RULES_EXAMPLE] [?USERNAME_RULES_EXAMPLE]
), ),
%% check length limit
{ok, 400, _} =
request(
post,
uri(["authorization", "sources", "built_in_database", "rules", "users"]),
[dup_rules_example(?USERNAME_RULES_EXAMPLE)]
),
{ok, 409, _} = {ok, 409, _} =
request( request(
post, post,
@ -171,6 +179,13 @@ t_api(_) ->
[?CLIENTID_RULES_EXAMPLE] [?CLIENTID_RULES_EXAMPLE]
), ),
{ok, 400, _} =
request(
post,
uri(["authorization", "sources", "built_in_database", "rules", "clients"]),
[dup_rules_example(?CLIENTID_RULES_EXAMPLE)]
),
{ok, 409, _} = {ok, 409, _} =
request( request(
post, post,
@ -238,6 +253,14 @@ t_api(_) ->
uri(["authorization", "sources", "built_in_database", "rules", "all"]), uri(["authorization", "sources", "built_in_database", "rules", "all"]),
?ALL_RULES_EXAMPLE ?ALL_RULES_EXAMPLE
), ),
{ok, 400, _} =
request(
post,
uri(["authorization", "sources", "built_in_database", "rules", "all"]),
dup_rules_example(?ALL_RULES_EXAMPLE)
),
{ok, 200, Request7} = {ok, 200, Request7} =
request( request(
get, get,
@ -491,3 +514,10 @@ replace_parts(Parts, Replacements) ->
end, end,
Parts Parts
). ).
dup_rules_example(#{username := _, rules := Rules}) ->
#{username => user2, rules => Rules ++ Rules};
dup_rules_example(#{clientid := _, rules := Rules}) ->
#{clientid => client2, rules => Rules ++ Rules};
dup_rules_example(#{rules := Rules}) ->
#{rules => Rules ++ Rules}.

View File

@ -6,4 +6,7 @@ builtin_db.desc:
builtin_db.label: builtin_db.label:
"""Builtin Database""" """Builtin Database"""
max_rules_len.desc:
"""Maximum rule length per client/user. Note that performance may decrease as rule length increases."""
} }