fix: return 404 if built_in_database not configured as auth source

This commit is contained in:
Stefan Strigler 2023-10-20 12:24:38 +02:00
parent af6a364492
commit 4e0e755b28
4 changed files with 337 additions and 130 deletions

View File

@ -49,6 +49,8 @@
aggregate_metrics/1 aggregate_metrics/1
]). ]).
-export([with_source/2]).
-define(TAGS, [<<"Authorization">>]). -define(TAGS, [<<"Authorization">>]).
api_spec() -> api_spec() ->

View File

@ -426,161 +426,210 @@ fields(rules) ->
%% HTTP API %% HTTP API
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
-define(IF_CONFIGURED_AUTHZ_SOURCE(EXPR),
emqx_authz_api_sources:with_source(
<<"built_in_database">>,
fun(_Source) ->
EXPR
end
)
).
users(get, #{query_string := QueryString}) -> users(get, #{query_string := QueryString}) ->
case ?IF_CONFIGURED_AUTHZ_SOURCE(
emqx_mgmt_api:node_query( case
node(), emqx_mgmt_api:node_query(
?ACL_TABLE, node(),
QueryString, ?ACL_TABLE,
?ACL_USERNAME_QSCHEMA, QueryString,
?QUERY_USERNAME_FUN, ?ACL_USERNAME_QSCHEMA,
fun ?MODULE:format_result/1 ?QUERY_USERNAME_FUN,
) fun ?MODULE:format_result/1
of )
{error, page_limit_invalid} -> of
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}}; {error, page_limit_invalid} ->
{error, Node, Error} -> {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, Error])), {error, Node, Error} ->
{500, #{code => <<"NODE_DOWN">>, message => Message}}; Message = list_to_binary(
Result -> io_lib:format("bad rpc call ~p, Reason ~p", [Node, Error])
{200, Result} ),
end; {500, #{code => <<"NODE_DOWN">>, message => Message}};
Result ->
{200, Result}
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 ?IF_CONFIGURED_AUTHZ_SOURCE(
[] -> case ensure_all_not_exists(<<"username">>, username, Body) of
lists:foreach( [] ->
fun(#{<<"username">> := Username, <<"rules">> := Rules}) -> lists:foreach(
emqx_authz_mnesia:store_rules({username, Username}, Rules) fun(#{<<"username">> := Username, <<"rules">> := Rules}) ->
end, emqx_authz_mnesia:store_rules({username, Username}, Rules)
Body end,
), Body
{204}; ),
Exists -> {204};
{409, #{ Exists ->
code => <<"ALREADY_EXISTS">>, {409, #{
message => binfmt("Users '~ts' already exist", [binjoin(Exists)]) code => <<"ALREADY_EXISTS">>,
}} message => binfmt("Users '~ts' already exist", [binjoin(Exists)])
end. }}
end
).
clients(get, #{query_string := QueryString}) -> clients(get, #{query_string := QueryString}) ->
case ?IF_CONFIGURED_AUTHZ_SOURCE(
emqx_mgmt_api:node_query( case
node(), emqx_mgmt_api:node_query(
?ACL_TABLE, node(),
QueryString, ?ACL_TABLE,
?ACL_CLIENTID_QSCHEMA, QueryString,
?QUERY_CLIENTID_FUN, ?ACL_CLIENTID_QSCHEMA,
fun ?MODULE:format_result/1 ?QUERY_CLIENTID_FUN,
) fun ?MODULE:format_result/1
of )
{error, page_limit_invalid} -> of
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}}; {error, page_limit_invalid} ->
{error, Node, Error} -> {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, Error])), {error, Node, Error} ->
{500, #{code => <<"NODE_DOWN">>, message => Message}}; Message = list_to_binary(
Result -> io_lib:format("bad rpc call ~p, Reason ~p", [Node, Error])
{200, Result} ),
end; {500, #{code => <<"NODE_DOWN">>, message => Message}};
Result ->
{200, Result}
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 ?IF_CONFIGURED_AUTHZ_SOURCE(
[] -> case ensure_all_not_exists(<<"clientid">>, clientid, Body) of
lists:foreach( [] ->
fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) -> lists:foreach(
emqx_authz_mnesia:store_rules({clientid, ClientID}, Rules) fun(#{<<"clientid">> := ClientID, <<"rules">> := Rules}) ->
end, emqx_authz_mnesia:store_rules({clientid, ClientID}, Rules)
Body end,
), Body
{204}; ),
Exists -> {204};
{409, #{ Exists ->
code => <<"ALREADY_EXISTS">>, {409, #{
message => binfmt("Clients '~ts' already exist", [binjoin(Exists)]) code => <<"ALREADY_EXISTS">>,
}} message => binfmt("Clients '~ts' already exist", [binjoin(Exists)])
end. }}
end
).
user(get, #{bindings := #{username := Username}}) -> user(get, #{bindings := #{username := Username}}) ->
case emqx_authz_mnesia:get_rules({username, Username}) of ?IF_CONFIGURED_AUTHZ_SOURCE(
not_found -> case emqx_authz_mnesia:get_rules({username, Username}) of
{404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}}; not_found ->
{ok, Rules} -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
{200, #{ {ok, Rules} ->
username => Username, {200, #{
rules => format_rules(Rules) username => Username,
}} rules => format_rules(Rules)
end; }}
end
);
user(put, #{ user(put, #{
bindings := #{username := Username}, bindings := #{username := Username},
body := #{<<"username">> := Username, <<"rules">> := Rules} body := #{<<"username">> := Username, <<"rules">> := Rules}
}) -> }) ->
emqx_authz_mnesia:store_rules({username, Username}, Rules), ?IF_CONFIGURED_AUTHZ_SOURCE(
{204}; begin
user(delete, #{bindings := #{username := Username}}) -> emqx_authz_mnesia:store_rules({username, Username}, Rules),
case emqx_authz_mnesia:get_rules({username, Username}) of
not_found ->
{404, #{code => <<"NOT_FOUND">>, message => <<"Username Not Found">>}};
{ok, _Rules} ->
emqx_authz_mnesia:delete_rules({username, Username}),
{204} {204}
end. end
);
user(delete, #{bindings := #{username := Username}}) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
case emqx_authz_mnesia:get_rules({username, Username}) of
not_found ->
{404, #{code => <<"NOT_FOUND">>, message => <<"Username Not Found">>}};
{ok, _Rules} ->
emqx_authz_mnesia:delete_rules({username, Username}),
{204}
end
).
client(get, #{bindings := #{clientid := ClientID}}) -> client(get, #{bindings := #{clientid := ClientID}}) ->
case emqx_authz_mnesia:get_rules({clientid, ClientID}) of ?IF_CONFIGURED_AUTHZ_SOURCE(
not_found -> case emqx_authz_mnesia:get_rules({clientid, ClientID}) of
{404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}}; not_found ->
{ok, Rules} -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
{200, #{ {ok, Rules} ->
clientid => ClientID, {200, #{
rules => format_rules(Rules) clientid => ClientID,
}} rules => format_rules(Rules)
end; }}
end
);
client(put, #{ client(put, #{
bindings := #{clientid := ClientID}, bindings := #{clientid := ClientID},
body := #{<<"clientid">> := ClientID, <<"rules">> := Rules} body := #{<<"clientid">> := ClientID, <<"rules">> := Rules}
}) -> }) ->
emqx_authz_mnesia:store_rules({clientid, ClientID}, Rules), ?IF_CONFIGURED_AUTHZ_SOURCE(
{204}; begin
client(delete, #{bindings := #{clientid := ClientID}}) -> emqx_authz_mnesia:store_rules({clientid, ClientID}, Rules),
case emqx_authz_mnesia:get_rules({clientid, ClientID}) of
not_found ->
{404, #{code => <<"NOT_FOUND">>, message => <<"ClientID Not Found">>}};
{ok, _Rules} ->
emqx_authz_mnesia:delete_rules({clientid, ClientID}),
{204} {204}
end. end
);
client(delete, #{bindings := #{clientid := ClientID}}) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
case emqx_authz_mnesia:get_rules({clientid, ClientID}) of
not_found ->
{404, #{code => <<"NOT_FOUND">>, message => <<"ClientID Not Found">>}};
{ok, _Rules} ->
emqx_authz_mnesia:delete_rules({clientid, ClientID}),
{204}
end
).
all(get, _) -> all(get, _) ->
case emqx_authz_mnesia:get_rules(all) of ?IF_CONFIGURED_AUTHZ_SOURCE(
not_found -> case emqx_authz_mnesia:get_rules(all) of
{200, #{rules => []}}; not_found ->
{ok, Rules} -> {200, #{rules => []}};
{200, #{ {ok, Rules} ->
rules => format_rules(Rules) {200, #{
}} rules => format_rules(Rules)
end; }}
end
);
all(post, #{body := #{<<"rules">> := Rules}}) -> all(post, #{body := #{<<"rules">> := Rules}}) ->
emqx_authz_mnesia:store_rules(all, Rules), ?IF_CONFIGURED_AUTHZ_SOURCE(
{204}; begin
emqx_authz_mnesia:store_rules(all, Rules),
{204}
end
);
all(delete, _) -> all(delete, _) ->
emqx_authz_mnesia:store_rules(all, []), ?IF_CONFIGURED_AUTHZ_SOURCE(
{204}. begin
emqx_authz_mnesia:store_rules(all, []),
{204}
end
).
rules(delete, _) -> rules(delete, _) ->
case emqx_authz_api_sources:get_raw_source(<<"built_in_database">>) of ?IF_CONFIGURED_AUTHZ_SOURCE(
[#{<<"enable">> := false}] -> case emqx_authz_api_sources:get_raw_source(<<"built_in_database">>) of
ok = emqx_authz_mnesia:purge_rules(), [#{<<"enable">> := false}] ->
{204}; ok = emqx_authz_mnesia:purge_rules(),
[#{<<"enable">> := true}] -> {204};
{400, #{ [#{<<"enable">> := true}] ->
code => <<"BAD_REQUEST">>, {400, #{
message => code => <<"BAD_REQUEST">>,
<<"'built_in_database' type source must be disabled before purge.">> message =>
}}; <<"'built_in_database' type source must be disabled before purge.">>
[] -> }};
{404, #{ [] ->
code => <<"BAD_REQUEST">>, {404, #{
message => <<"'built_in_database' type source is not found.">> code => <<"BAD_REQUEST">>,
}} message => <<"'built_in_database' type source is not found.">>
end. }}
end
).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% QueryString to MatchSpec %% QueryString to MatchSpec

View File

@ -331,4 +331,159 @@ t_api(_) ->
[] []
), ),
?assertEqual(0, emqx_authz_mnesia:record_count()), ?assertEqual(0, emqx_authz_mnesia:record_count()),
Examples = make_examples(emqx_authz_api_mnesia),
?assertEqual(
14,
length(Examples)
),
Fixtures1 = fun() ->
{ok, _, _} =
request(
delete,
uri(["authorization", "sources", "built_in_database", "rules", "all"]),
[]
),
{ok, _, _} =
request(
delete,
uri(["authorization", "sources", "built_in_database", "rules", "users"]),
[]
),
{ok, _, _} =
request(
delete,
uri(["authorization", "sources", "built_in_database", "rules", "clients"]),
[]
)
end,
run_examples(Examples, Fixtures1),
Fixtures2 = fun() ->
%% disable/remove built_in_database
{ok, 204, _} =
request(
delete,
uri(["authorization", "sources", "built_in_database"]),
[]
)
end,
run_examples(404, Examples, Fixtures2),
ok. ok.
%% test helpers
-define(REPLACEMENTS, #{
":clientid" => <<"client1">>,
":username" => <<"user1">>
}).
run_examples(Examples) ->
%% assume all ok
run_examples(
fun
({ok, Code, _}) when
Code >= 200,
Code =< 299
->
true;
(_Res) ->
ct:pal("check failed: ~p", [_Res]),
false
end,
Examples
).
run_examples(Examples, Fixtures) when is_function(Fixtures) ->
Fixtures(),
run_examples(Examples);
run_examples(Check, Examples) when is_function(Check) ->
lists:foreach(
fun({Path, Op, Body} = _Req) ->
ct:pal("req: ~p", [_Req]),
?assert(
Check(
request(Op, uri(Path), Body)
)
)
end,
Examples
);
run_examples(Code, Examples) when is_number(Code) ->
run_examples(
fun
({ok, ResCode, _}) when Code =:= ResCode -> true;
(_) -> false
end,
Examples
).
run_examples(CodeOrCheck, Examples, Fixtures) when is_function(Fixtures) ->
Fixtures(),
run_examples(CodeOrCheck, Examples).
make_examples(ApiMod) ->
make_examples(ApiMod, ?REPLACEMENTS).
-spec make_examples(Mod :: atom()) -> [{Path :: list(), [{Op :: atom(), Body :: term()}]}].
make_examples(ApiMod, Replacements) ->
Paths = ApiMod:paths(),
lists:flatten(
lists:map(
fun(Path) ->
Schema = ApiMod:schema(Path),
lists:map(
fun({Op, OpSchema}) ->
Body =
case maps:get('requestBody', OpSchema, undefined) of
undefined ->
[];
HoconWithExamples ->
maps:get(
value,
hd(
maps:values(
maps:get(
<<"examples">>,
maps:get(examples, HoconWithExamples)
)
)
)
)
end,
{replace_parts(to_parts(Path), Replacements), Op, Body}
end,
lists:sort(fun op_sort/2, maps:to_list(maps:remove('operationId', Schema)))
)
end,
Paths
)
).
op_sort({post, _}, {_, _}) ->
true;
op_sort({put, _}, {_, _}) ->
true;
op_sort({get, _}, {delete, _}) ->
true;
op_sort(_, _) ->
false.
to_parts(Path) ->
string:tokens(Path, "/").
replace_parts(Parts, Replacements) ->
lists:map(
fun(Part) ->
%% that's the fun part
case maps:is_key(Part, Replacements) of
true ->
maps:get(Part, Replacements);
false ->
Part
end
end,
Parts
).

View File

@ -0,0 +1 @@
Modified HTTP API behavior for APIs managing the `built_in_database` authorization source: They will now return a `404` status code if `built_in_database` is not set as the authorization source, replacing the former `20X` response.