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,7 +426,17 @@ 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}) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
case case
emqx_mgmt_api:node_query( emqx_mgmt_api:node_query(
node(), node(),
@ -440,12 +450,16 @@ users(get, #{query_string := QueryString}) ->
{error, page_limit_invalid} -> {error, page_limit_invalid} ->
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}}; {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
{error, Node, Error} -> {error, Node, Error} ->
Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, Error])), Message = list_to_binary(
io_lib:format("bad rpc call ~p, Reason ~p", [Node, Error])
),
{500, #{code => <<"NODE_DOWN">>, message => Message}}; {500, #{code => <<"NODE_DOWN">>, message => Message}};
Result -> Result ->
{200, Result} {200, Result}
end; end
);
users(post, #{body := Body}) when is_list(Body) -> users(post, #{body := Body}) when is_list(Body) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
case ensure_all_not_exists(<<"username">>, username, Body) of case ensure_all_not_exists(<<"username">>, username, Body) of
[] -> [] ->
lists:foreach( lists:foreach(
@ -460,9 +474,11 @@ users(post, #{body := Body}) when is_list(Body) ->
code => <<"ALREADY_EXISTS">>, code => <<"ALREADY_EXISTS">>,
message => binfmt("Users '~ts' already exist", [binjoin(Exists)]) message => binfmt("Users '~ts' already exist", [binjoin(Exists)])
}} }}
end. end
).
clients(get, #{query_string := QueryString}) -> clients(get, #{query_string := QueryString}) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
case case
emqx_mgmt_api:node_query( emqx_mgmt_api:node_query(
node(), node(),
@ -476,12 +492,16 @@ clients(get, #{query_string := QueryString}) ->
{error, page_limit_invalid} -> {error, page_limit_invalid} ->
{400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}}; {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
{error, Node, Error} -> {error, Node, Error} ->
Message = list_to_binary(io_lib:format("bad rpc call ~p, Reason ~p", [Node, Error])), Message = list_to_binary(
io_lib:format("bad rpc call ~p, Reason ~p", [Node, Error])
),
{500, #{code => <<"NODE_DOWN">>, message => Message}}; {500, #{code => <<"NODE_DOWN">>, message => Message}};
Result -> Result ->
{200, Result} {200, Result}
end; end
);
clients(post, #{body := Body}) when is_list(Body) -> clients(post, #{body := Body}) when is_list(Body) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
case ensure_all_not_exists(<<"clientid">>, clientid, Body) of case ensure_all_not_exists(<<"clientid">>, clientid, Body) of
[] -> [] ->
lists:foreach( lists:foreach(
@ -496,9 +516,11 @@ clients(post, #{body := Body}) when is_list(Body) ->
code => <<"ALREADY_EXISTS">>, code => <<"ALREADY_EXISTS">>,
message => binfmt("Clients '~ts' already exist", [binjoin(Exists)]) message => binfmt("Clients '~ts' already exist", [binjoin(Exists)])
}} }}
end. end
).
user(get, #{bindings := #{username := Username}}) -> user(get, #{bindings := #{username := Username}}) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
case emqx_authz_mnesia:get_rules({username, Username}) of case emqx_authz_mnesia:get_rules({username, Username}) of
not_found -> not_found ->
{404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}}; {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
@ -507,23 +529,31 @@ user(get, #{bindings := #{username := Username}}) ->
username => Username, username => Username,
rules => format_rules(Rules) 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}
}) -> }) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
begin
emqx_authz_mnesia:store_rules({username, Username}, Rules), emqx_authz_mnesia:store_rules({username, Username}, Rules),
{204}; {204}
end
);
user(delete, #{bindings := #{username := Username}}) -> user(delete, #{bindings := #{username := Username}}) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
case emqx_authz_mnesia:get_rules({username, Username}) of case emqx_authz_mnesia:get_rules({username, Username}) of
not_found -> not_found ->
{404, #{code => <<"NOT_FOUND">>, message => <<"Username Not Found">>}}; {404, #{code => <<"NOT_FOUND">>, message => <<"Username Not Found">>}};
{ok, _Rules} -> {ok, _Rules} ->
emqx_authz_mnesia:delete_rules({username, Username}), emqx_authz_mnesia:delete_rules({username, Username}),
{204} {204}
end. end
).
client(get, #{bindings := #{clientid := ClientID}}) -> client(get, #{bindings := #{clientid := ClientID}}) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
case emqx_authz_mnesia:get_rules({clientid, ClientID}) of case emqx_authz_mnesia:get_rules({clientid, ClientID}) of
not_found -> not_found ->
{404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}}; {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
@ -532,23 +562,31 @@ client(get, #{bindings := #{clientid := ClientID}}) ->
clientid => ClientID, clientid => ClientID,
rules => format_rules(Rules) 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}
}) -> }) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
begin
emqx_authz_mnesia:store_rules({clientid, ClientID}, Rules), emqx_authz_mnesia:store_rules({clientid, ClientID}, Rules),
{204}; {204}
end
);
client(delete, #{bindings := #{clientid := ClientID}}) -> client(delete, #{bindings := #{clientid := ClientID}}) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
case emqx_authz_mnesia:get_rules({clientid, ClientID}) of case emqx_authz_mnesia:get_rules({clientid, ClientID}) of
not_found -> not_found ->
{404, #{code => <<"NOT_FOUND">>, message => <<"ClientID Not Found">>}}; {404, #{code => <<"NOT_FOUND">>, message => <<"ClientID Not Found">>}};
{ok, _Rules} -> {ok, _Rules} ->
emqx_authz_mnesia:delete_rules({clientid, ClientID}), emqx_authz_mnesia:delete_rules({clientid, ClientID}),
{204} {204}
end. end
).
all(get, _) -> all(get, _) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
case emqx_authz_mnesia:get_rules(all) of case emqx_authz_mnesia:get_rules(all) of
not_found -> not_found ->
{200, #{rules => []}}; {200, #{rules => []}};
@ -556,15 +594,25 @@ all(get, _) ->
{200, #{ {200, #{
rules => format_rules(Rules) rules => format_rules(Rules)
}} }}
end; end
);
all(post, #{body := #{<<"rules">> := Rules}}) -> all(post, #{body := #{<<"rules">> := Rules}}) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
begin
emqx_authz_mnesia:store_rules(all, Rules), emqx_authz_mnesia:store_rules(all, Rules),
{204}; {204}
end
);
all(delete, _) -> all(delete, _) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
begin
emqx_authz_mnesia:store_rules(all, []), emqx_authz_mnesia:store_rules(all, []),
{204}. {204}
end
).
rules(delete, _) -> rules(delete, _) ->
?IF_CONFIGURED_AUTHZ_SOURCE(
case emqx_authz_api_sources:get_raw_source(<<"built_in_database">>) of case emqx_authz_api_sources:get_raw_source(<<"built_in_database">>) of
[#{<<"enable">> := false}] -> [#{<<"enable">> := false}] ->
ok = emqx_authz_mnesia:purge_rules(), ok = emqx_authz_mnesia:purge_rules(),
@ -580,7 +628,8 @@ rules(delete, _) ->
code => <<"BAD_REQUEST">>, code => <<"BAD_REQUEST">>,
message => <<"'built_in_database' type source is not found.">> 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.