feat: add authn, authz status api

This commit is contained in:
EMQ-YangM 2022-03-17 16:35:27 +08:00
parent e6fcef16ba
commit d1857ba454
5 changed files with 233 additions and 72 deletions

View File

@ -49,8 +49,10 @@
-export([ authenticators/2 -export([ authenticators/2
, authenticator/2 , authenticator/2
, authenticator_status/2
, listener_authenticators/2 , listener_authenticators/2
, listener_authenticator/2 , listener_authenticator/2
, listener_authenticator_status/2
, authenticator_move/2 , authenticator_move/2
, listener_authenticator_move/2 , listener_authenticator_move/2
, authenticator_import_users/2 , authenticator_import_users/2
@ -88,6 +90,7 @@ api_spec() ->
paths() -> [ "/authentication" paths() -> [ "/authentication"
, "/authentication/:id" , "/authentication/:id"
, "/authentication/:id/status"
, "/authentication/:id/move" , "/authentication/:id/move"
, "/authentication/:id/import_users" , "/authentication/:id/import_users"
, "/authentication/:id/users" , "/authentication/:id/users"
@ -95,6 +98,7 @@ paths() -> [ "/authentication"
, "/listeners/:listener_id/authentication" , "/listeners/:listener_id/authentication"
, "/listeners/:listener_id/authentication/:id" , "/listeners/:listener_id/authentication/:id"
, "/listeners/:listener_id/authentication/:id/status"
, "/listeners/:listener_id/authentication/:id/move" , "/listeners/:listener_id/authentication/:id/move"
, "/listeners/:listener_id/authentication/:id/import_users" , "/listeners/:listener_id/authentication/:id/import_users"
, "/listeners/:listener_id/authentication/:id/users" , "/listeners/:listener_id/authentication/:id/users"
@ -213,6 +217,22 @@ schema("/authentication/:id") ->
} }
}; };
schema("/authentication/:id/status") ->
#{
'operationId' => authenticator_status,
get => #{
tags => ?API_TAGS_GLOBAL,
description => <<"Get authenticator status from global authentication chain">>,
parameters => [param_auth_id()],
responses => #{
200 => emqx_dashboard_swagger:schema_with_examples(
hoconsc:ref(emqx_authn_schema, "metrics_status_fields"),
status_metrics_example()),
400 => error_codes([?BAD_REQUEST], <<"Bad Request">>)
}
}
};
schema("/listeners/:listener_id/authentication") -> schema("/listeners/:listener_id/authentication") ->
#{ #{
'operationId' => listener_authenticators, 'operationId' => listener_authenticators,
@ -285,6 +305,21 @@ schema("/listeners/:listener_id/authentication/:id") ->
} }
}; };
schema("/listeners/:listener_id/authentication/:id/status") ->
#{
'operationId' => listener_authenticator_status,
get => #{
tags => ?API_TAGS_SINGLE,
description => <<"Get authenticator status from listener authentication chain">>,
parameters => [param_listener_id(), param_auth_id()],
responses => #{
200 => emqx_dashboard_swagger:schema_with_examples(
hoconsc:ref(emqx_authn_schema, "metrics_status_fields"),
status_metrics_example()),
400 => error_codes([?BAD_REQUEST], <<"Bad Request">>)
}
}
};
schema("/authentication/:id/move") -> schema("/authentication/:id/move") ->
#{ #{
@ -560,6 +595,9 @@ authenticator(put, #{bindings := #{id := AuthenticatorID}, body := Config}) ->
authenticator(delete, #{bindings := #{id := AuthenticatorID}}) -> authenticator(delete, #{bindings := #{id := AuthenticatorID}}) ->
delete_authenticator([authentication], ?GLOBAL, AuthenticatorID). delete_authenticator([authentication], ?GLOBAL, AuthenticatorID).
authenticator_status(get, #{bindings := #{id := AuthenticatorID}}) ->
lookup_from_all_nodes(?GLOBAL, AuthenticatorID).
listener_authenticators(post, #{bindings := #{listener_id := ListenerID}, body := Config}) -> listener_authenticators(post, #{bindings := #{listener_id := ListenerID}, body := Config}) ->
with_listener(ListenerID, with_listener(ListenerID,
fun(Type, Name, ChainName) -> fun(Type, Name, ChainName) ->
@ -599,6 +637,13 @@ listener_authenticator(delete,
AuthenticatorID) AuthenticatorID)
end). end).
listener_authenticator_status(get,
#{bindings := #{listener_id := ListenerID, id := AuthenticatorID}}) ->
with_listener(ListenerID,
fun(_, _, ChainName) ->
lookup_from_all_nodes(ChainName, AuthenticatorID)
end).
authenticator_move(post, authenticator_move(post,
#{bindings := #{id := AuthenticatorID}, #{bindings := #{id := AuthenticatorID},
body := #{<<"position">> := Position}}) -> body := #{<<"position">> := Position}}) ->
@ -797,16 +842,16 @@ lookup_from_all_nodes(ChainName, AuthenticatorID) ->
MKMap = fun (Name) -> fun ({Key, Val}) -> #{ node => Key, Name => Val } end end, MKMap = fun (Name) -> fun ({Key, Val}) -> #{ node => Key, Name => Val } end end,
HelpFun = fun (M, Name) -> lists:map(MKMap(Name), maps:to_list(M)) end, HelpFun = fun (M, Name) -> lists:map(MKMap(Name), maps:to_list(M)) end,
case AggregateStatus of case AggregateStatus of
empty_metrics_and_status -> {ok, #{}}; empty_metrics_and_status -> serialize_error(unsupported_operation);
_ -> {ok, #{node_status => HelpFun(StatusMap, status), _ -> {200, #{node_status => HelpFun(StatusMap, status),
node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics), node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics),
status => AggregateStatus, status => AggregateStatus,
metrics => restructure_map(AggregateMetrics) metrics => restructure_map(AggregateMetrics)
} }
} }
end; end;
{error, ErrL} -> {error, ErrL} ->
{error, error_msg('INTERNAL_ERROR', ErrL)} serialize_error(ErrL)
end. end.
aggregate_status([]) -> empty_metrics_and_status; aggregate_status([]) -> empty_metrics_and_status;
@ -865,12 +910,6 @@ restructure_map(#{counters := #{failed := Failed, matched := Match, success := S
restructure_map(Error) -> restructure_map(Error) ->
Error. Error.
error_msg(Code, Msg) ->
#{code => Code, message => bin(io_lib:format("~p", [Msg]))}.
bin(S) when is_list(S) ->
list_to_binary(S).
is_ok(ResL) -> is_ok(ResL) ->
case lists:filter(fun({ok, _}) -> false; (_) -> true end, ResL) of case lists:filter(fun({ok, _}) -> false; (_) -> true end, ResL) of
[] -> {ok, [Res || {ok, Res} <- ResL]}; [] -> {ok, [Res || {ok, Res} <- ResL]};
@ -1169,6 +1208,31 @@ authenticator_examples() ->
} }
}. }.
status_metrics_example() ->
#{ metrics => #{ matched => 0,
success => 0,
failed => 0,
rate => 0.0,
rate_last5m => 0.0,
rate_max => 0.0
},
node_metrics => [ #{node => node(),
metrics => #{ matched => 0,
success => 0,
failed => 0,
rate => 0.0,
rate_last5m => 0.0,
rate_max => 0.0
}
}
],
status => connected,
node_status => [ #{node => node(),
status => connected
}
]
}.
request_user_create_examples() -> request_user_create_examples() ->
#{ #{
regular_user => #{ regular_user => #{

View File

@ -17,6 +17,7 @@
-module(emqx_authn_schema). -module(emqx_authn_schema).
-include_lib("typerefl/include/types.hrl"). -include_lib("typerefl/include/types.hrl").
-import(hoconsc, [mk/2, ref/2]).
-export([ common_fields/0 -export([ common_fields/0
, roots/0 , roots/0
@ -29,7 +30,6 @@
roots() -> []. roots() -> [].
fields(_) -> [].
common_fields() -> common_fields() ->
[ {enable, fun enable/1} [ {enable, fun enable/1}
@ -59,3 +59,40 @@ mechanism(Name) ->
backend(Name) -> backend(Name) ->
hoconsc:mk(hoconsc:enum([Name]), hoconsc:mk(hoconsc:enum([Name]),
#{required => true}). #{required => true}).
fields("metrics_status_fields") ->
[ {"metrics", mk(ref(?MODULE, "metrics"), #{desc => "The metrics of the resource"})}
, {"node_metrics", mk(hoconsc:array(ref(?MODULE, "node_metrics")),
#{ desc => "The metrics of the resource for each node"
})}
, {"status", mk(status(), #{desc => "The status of the resource"})}
, {"node_status", mk(hoconsc:array(ref(?MODULE, "node_status")),
#{ desc => "The status of the resource for each node"
})}
];
fields("metrics") ->
[ {"matched", mk(integer(), #{desc => "Count of this resource is queried"})}
, {"success", mk(integer(), #{desc => "Count of query success"})}
, {"failed", mk(integer(), #{desc => "Count of query failed"})}
, {"rate", mk(float(), #{desc => "The rate of matched, times/second"})}
, {"rate_max", mk(float(), #{desc => "The max rate of matched, times/second"})}
, {"rate_last5m", mk(float(),
#{desc => "The average rate of matched in the last 5 minutes, times/second"})}
];
fields("node_metrics") ->
[ node_name()
, {"metrics", mk(ref(?MODULE, "metrics"), #{})}
];
fields("node_status") ->
[ node_name()
, {"status", mk(status(), #{})}
].
status() ->
hoconsc:enum([connected, disconnected, connecting]).
node_name() ->
{"node", mk(binary(), #{desc => "The node name", example => "emqx@127.0.0.1"})}.

View File

@ -167,30 +167,34 @@ test_authenticator(PathPrefix) ->
{ok, 200, _} = request( {ok, 200, _} = request(
get, get,
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])), uri(PathPrefix ++ [?CONF_NS, "password_based:http"])),
%% {ok, RList} = emqx_json:safe_decode(Res),
%% Snd = fun ({_, Val}) -> Val end, {ok, 200, Res} = request(
%% LookupVal = fun LookupV(List, RestJson) -> get,
%% case List of uri(PathPrefix ++ [?CONF_NS, "password_based:http", "status"])),
%% [Name] -> Snd(lists:keyfind(Name, 1, RestJson)); {ok, RList} = emqx_json:safe_decode(Res),
%% [Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson))) Snd = fun ({_, Val}) -> Val end,
%% end LookupVal = fun LookupV(List, RestJson) ->
%% end, case List of
%% LookFun = fun (List) -> LookupVal(List, RList) end, [Name] -> Snd(lists:keyfind(Name, 1, RestJson));
%% MetricsList = [{<<"failed">>, 0}, [Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
%% {<<"matched">>, 0}, end
%% {<<"rate">>, 0.0}, end,
%% {<<"rate_last5m">>, 0.0}, LookFun = fun (List) -> LookupVal(List, RList) end,
%% {<<"rate_max">>, 0.0}, MetricsList = [{<<"failed">>, 0},
%% {<<"success">>, 0}], {<<"matched">>, 0},
%% EqualFun = fun ({M, V}) -> {<<"rate">>, 0.0},
%% ?assertEqual(V, LookFun([<<"metrics">>, {<<"rate_last5m">>, 0.0},
%% M] {<<"rate_max">>, 0.0},
%% ) {<<"success">>, 0}],
%% ) end, EqualFun = fun ({M, V}) ->
%% lists:map(EqualFun, MetricsList), ?assertEqual(V, LookFun([<<"metrics">>,
%% ?assertEqual(<<"connected">>, M]
%% LookFun([<<"status">> )
%% ])), ) end,
lists:map(EqualFun, MetricsList),
?assertEqual(<<"connected">>,
LookFun([<<"status">>
])),
{ok, 404, _} = request( {ok, 404, _} = request(
get, get,
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])), uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])),

View File

@ -54,6 +54,7 @@
-export([ get_raw_sources/0 -export([ get_raw_sources/0
, get_raw_source/1 , get_raw_source/1
, source_status/2
, lookup_from_local_node/1 , lookup_from_local_node/1
, lookup_from_all_nodes/1 , lookup_from_all_nodes/1
]). ]).
@ -74,6 +75,7 @@ api_spec() ->
paths() -> paths() ->
[ "/authorization/sources" [ "/authorization/sources"
, "/authorization/sources/:type" , "/authorization/sources/:type"
, "/authorization/sources/:type/status"
, "/authorization/sources/:type/move"]. , "/authorization/sources/:type/move"].
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -148,6 +150,19 @@ schema("/authorization/sources/:type") ->
} }
} }
}; };
schema("/authorization/sources/:type/status") ->
#{ 'operationId' => source_status
, get =>
#{ description => <<"Get a authorization source">>
, parameters => parameters_field()
, responses =>
#{ 200 => emqx_dashboard_swagger:schema_with_examples(
hoconsc:ref(emqx_authn_schema, "metrics_status_fields"),
status_metrics_example())
, 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad request">>)
}
}
};
schema("/authorization/sources/:type/move") -> schema("/authorization/sources/:type/move") ->
#{ 'operationId' => move_source #{ 'operationId' => move_source
, post => , post =>
@ -250,6 +265,21 @@ source(put, #{bindings := #{type := Type}, body := Body}) when is_map(Body) ->
source(delete, #{bindings := #{type := Type}}) -> source(delete, #{bindings := #{type := Type}}) ->
update_config({?CMD_DELETE, Type}, #{}). update_config({?CMD_DELETE, Type}, #{}).
source_status(get, #{bindings := #{type := Type}}) ->
BinType = atom_to_binary(Type, utf8),
case get_raw_source(BinType) of
[] -> {400, #{code => <<"BAD_REQUEST">>,
message => <<"Not found", BinType/binary>>}};
[#{<<"type">> := <<"file">>}] ->
{400, #{code => <<"BAD_REQUEST">>,
message => <<"Not Support Status">>}};
[_] ->
case emqx_authz:lookup(Type) of
#{annotations := #{id := ResourceId }} -> lookup_from_all_nodes(ResourceId);
_ -> {400, #{code => <<"BAD_REQUEST">>, message => <<"Resource Disable">>}}
end
end.
move_source(Method, #{bindings := #{type := Type} = Bindings } = Req) move_source(Method, #{bindings := #{type := Type} = Bindings } = Req)
when is_atom(Type) -> when is_atom(Type) ->
move_source(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}}); move_source(Method, Req#{bindings => Bindings#{type => atom_to_binary(Type, utf8)}});
@ -301,8 +331,9 @@ lookup_from_all_nodes(ResourceId) ->
MKMap = fun (Name) -> fun ({Key, Val}) -> #{ node => Key, Name => Val } end end, MKMap = fun (Name) -> fun ({Key, Val}) -> #{ node => Key, Name => Val } end end,
HelpFun = fun (M, Name) -> lists:map(MKMap(Name), maps:to_list(M)) end, HelpFun = fun (M, Name) -> lists:map(MKMap(Name), maps:to_list(M)) end,
case AggregateStatus of case AggregateStatus of
empty_metrics_and_status -> {ok, #{}}; empty_metrics_and_status -> {400, #{code => <<"BAD_REQUEST">>,
_ -> {ok, #{node_status => HelpFun(StatusMap, status), message => <<"Resource Not Support Status">>}};
_ -> {200, #{node_status => HelpFun(StatusMap, status),
node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics), node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics),
status => AggregateStatus, status => AggregateStatus,
metrics => restructure_map(AggregateMetrics) metrics => restructure_map(AggregateMetrics)
@ -310,7 +341,8 @@ lookup_from_all_nodes(ResourceId) ->
} }
end; end;
{error, ErrL} -> {error, ErrL} ->
{error, error_msg('INTERNAL_ERROR', ErrL)} {400, #{code => <<"BAD_REQUEST">>,
message => bin_t(io_lib:format("~p", [ErrL]))}}
end. end.
aggregate_status([]) -> empty_metrics_and_status; aggregate_status([]) -> empty_metrics_and_status;
@ -369,9 +401,6 @@ restructure_map(#{counters := #{failed := Failed, matched := Match, success := S
restructure_map(Error) -> restructure_map(Error) ->
Error. Error.
error_msg(Code, Msg) ->
#{code => Code, message => bin_t(io_lib:format("~p", [Msg]))}.
bin_t(S) when is_list(S) -> bin_t(S) when is_list(S) ->
list_to_binary(S). list_to_binary(S).
@ -507,3 +536,28 @@ authz_sources_types(Type) ->
emqx_authz_api_schema:authz_sources_types(Type). emqx_authz_api_schema:authz_sources_types(Type).
bin(Term) -> erlang:iolist_to_binary(io_lib:format("~p", [Term])). bin(Term) -> erlang:iolist_to_binary(io_lib:format("~p", [Term])).
status_metrics_example() ->
#{ metrics => #{ matched => 0,
success => 0,
failed => 0,
rate => 0.0,
rate_last5m => 0.0,
rate_max => 0.0
},
node_metrics => [ #{node => node(),
metrics => #{ matched => 0,
success => 0,
failed => 0,
rate => 0.0,
rate_last5m => 0.0,
rate_max => 0.0
}
}
],
status => connected,
node_status => [ #{node => node(),
status => connected
}
]
}.

View File

@ -176,32 +176,32 @@ t_api(_) ->
[?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), [?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]),
{ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1), {ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1),
%% Snd = fun ({_, Val}) -> Val end, Snd = fun ({_, Val}) -> Val end,
%% LookupVal = fun LookupV(List, RestJson) -> LookupVal = fun LookupV(List, RestJson) ->
%% case List of case List of
%% [Name] -> Snd(lists:keyfind(Name, 1, RestJson)); [Name] -> Snd(lists:keyfind(Name, 1, RestJson));
%% [Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson))) [Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
%% end end
%% end, end,
%% EqualFun = fun (RList) -> EqualFun = fun (RList) ->
%% fun ({M, V}) -> fun ({M, V}) ->
%% ?assertEqual(V, ?assertEqual(V,
%% LookupVal([<<"metrics">>, M], LookupVal([<<"metrics">>, M],
%% RList) RList)
%% ) )
%% end end
%% end, end,
%% AssertFun = AssertFun =
%% fun (ResultJson) -> fun (ResultJson) ->
%% {ok, RList} = emqx_json:safe_decode(ResultJson), {ok, RList} = emqx_json:safe_decode(ResultJson),
%% MetricsList = [{<<"failed">>, 0}, MetricsList = [{<<"failed">>, 0},
%% {<<"matched">>, 0}, {<<"matched">>, 0},
%% {<<"rate">>, 0.0}, {<<"rate">>, 0.0},
%% {<<"rate_last5m">>, 0.0}, {<<"rate_last5m">>, 0.0},
%% {<<"rate_max">>, 0.0}, {<<"rate_max">>, 0.0},
%% {<<"success">>, 0}], {<<"success">>, 0}],
%% lists:map(EqualFun(RList), MetricsList) lists:map(EqualFun(RList), MetricsList)
%% end, end,
{ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []), {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []),
Sources = get_sources(Result2), Sources = get_sources(Result2),
@ -238,7 +238,8 @@ t_api(_) ->
<<"verify">> => <<"verify_none">> <<"verify">> => <<"verify_none">>
}}), }}),
{ok, 200, Result4} = request(get, uri(["authorization", "sources", "mongodb"]), []), {ok, 200, Result4} = request(get, uri(["authorization", "sources", "mongodb"]), []),
%% AssertFun(Result4), {ok, 200, Status4} = request(get, uri(["authorization", "sources", "mongodb", "status"]), []),
AssertFun(Status4),
?assertMatch(#{<<"type">> := <<"mongodb">>, ?assertMatch(#{<<"type">> := <<"mongodb">>,
<<"ssl">> := #{<<"enable">> := <<"true">>, <<"ssl">> := #{<<"enable">> := <<"true">>,
<<"cacertfile">> := ?MATCH_CERT, <<"cacertfile">> := ?MATCH_CERT,
@ -261,7 +262,8 @@ t_api(_) ->
<<"verify">> => <<"verify_none">> <<"verify">> => <<"verify_none">>
}}), }}),
{ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongodb"]), []), {ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongodb"]), []),
%% AssertFun(Result5), {ok, 200, Status5} = request(get, uri(["authorization", "sources", "mongodb", "status"]), []),
AssertFun(Status5),
?assertMatch(#{<<"type">> := <<"mongodb">>, ?assertMatch(#{<<"type">> := <<"mongodb">>,
<<"ssl">> := #{<<"enable">> := <<"true">>, <<"ssl">> := #{<<"enable">> := <<"true">>,
<<"cacertfile">> := ?MATCH_CERT, <<"cacertfile">> := ?MATCH_CERT,