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
, authenticator/2
, authenticator_status/2
, listener_authenticators/2
, listener_authenticator/2
, listener_authenticator_status/2
, authenticator_move/2
, listener_authenticator_move/2
, authenticator_import_users/2
@ -88,6 +90,7 @@ api_spec() ->
paths() -> [ "/authentication"
, "/authentication/:id"
, "/authentication/:id/status"
, "/authentication/:id/move"
, "/authentication/:id/import_users"
, "/authentication/:id/users"
@ -95,6 +98,7 @@ paths() -> [ "/authentication"
, "/listeners/:listener_id/authentication"
, "/listeners/:listener_id/authentication/:id"
, "/listeners/:listener_id/authentication/:id/status"
, "/listeners/:listener_id/authentication/:id/move"
, "/listeners/:listener_id/authentication/:id/import_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") ->
#{
'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") ->
#{
@ -560,6 +595,9 @@ authenticator(put, #{bindings := #{id := AuthenticatorID}, body := Config}) ->
authenticator(delete, #{bindings := #{id := 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}) ->
with_listener(ListenerID,
fun(Type, Name, ChainName) ->
@ -599,6 +637,13 @@ listener_authenticator(delete,
AuthenticatorID)
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,
#{bindings := #{id := AuthenticatorID},
body := #{<<"position">> := Position}}) ->
@ -797,8 +842,8 @@ lookup_from_all_nodes(ChainName, AuthenticatorID) ->
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,
case AggregateStatus of
empty_metrics_and_status -> {ok, #{}};
_ -> {ok, #{node_status => HelpFun(StatusMap, status),
empty_metrics_and_status -> serialize_error(unsupported_operation);
_ -> {200, #{node_status => HelpFun(StatusMap, status),
node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics),
status => AggregateStatus,
metrics => restructure_map(AggregateMetrics)
@ -806,7 +851,7 @@ lookup_from_all_nodes(ChainName, AuthenticatorID) ->
}
end;
{error, ErrL} ->
{error, error_msg('INTERNAL_ERROR', ErrL)}
serialize_error(ErrL)
end.
aggregate_status([]) -> empty_metrics_and_status;
@ -865,12 +910,6 @@ restructure_map(#{counters := #{failed := Failed, matched := Match, success := S
restructure_map(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) ->
case lists:filter(fun({ok, _}) -> false; (_) -> true end, ResL) of
[] -> {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() ->
#{
regular_user => #{

View File

@ -17,6 +17,7 @@
-module(emqx_authn_schema).
-include_lib("typerefl/include/types.hrl").
-import(hoconsc, [mk/2, ref/2]).
-export([ common_fields/0
, roots/0
@ -29,7 +30,6 @@
roots() -> [].
fields(_) -> [].
common_fields() ->
[ {enable, fun enable/1}
@ -59,3 +59,40 @@ mechanism(Name) ->
backend(Name) ->
hoconsc:mk(hoconsc:enum([Name]),
#{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(
get,
uri(PathPrefix ++ [?CONF_NS, "password_based:http"])),
%% {ok, RList} = emqx_json:safe_decode(Res),
%% Snd = fun ({_, Val}) -> Val end,
%% LookupVal = fun LookupV(List, RestJson) ->
%% case List of
%% [Name] -> Snd(lists:keyfind(Name, 1, RestJson));
%% [Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
%% end
%% end,
%% LookFun = fun (List) -> LookupVal(List, RList) end,
%% MetricsList = [{<<"failed">>, 0},
%% {<<"matched">>, 0},
%% {<<"rate">>, 0.0},
%% {<<"rate_last5m">>, 0.0},
%% {<<"rate_max">>, 0.0},
%% {<<"success">>, 0}],
%% EqualFun = fun ({M, V}) ->
%% ?assertEqual(V, LookFun([<<"metrics">>,
%% M]
%% )
%% ) end,
%% lists:map(EqualFun, MetricsList),
%% ?assertEqual(<<"connected">>,
%% LookFun([<<"status">>
%% ])),
{ok, 200, Res} = request(
get,
uri(PathPrefix ++ [?CONF_NS, "password_based:http", "status"])),
{ok, RList} = emqx_json:safe_decode(Res),
Snd = fun ({_, Val}) -> Val end,
LookupVal = fun LookupV(List, RestJson) ->
case List of
[Name] -> Snd(lists:keyfind(Name, 1, RestJson));
[Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
end
end,
LookFun = fun (List) -> LookupVal(List, RList) end,
MetricsList = [{<<"failed">>, 0},
{<<"matched">>, 0},
{<<"rate">>, 0.0},
{<<"rate_last5m">>, 0.0},
{<<"rate_max">>, 0.0},
{<<"success">>, 0}],
EqualFun = fun ({M, V}) ->
?assertEqual(V, LookFun([<<"metrics">>,
M]
)
) end,
lists:map(EqualFun, MetricsList),
?assertEqual(<<"connected">>,
LookFun([<<"status">>
])),
{ok, 404, _} = request(
get,
uri(PathPrefix ++ [?CONF_NS, "password_based:redis"])),

View File

@ -54,6 +54,7 @@
-export([ get_raw_sources/0
, get_raw_source/1
, source_status/2
, lookup_from_local_node/1
, lookup_from_all_nodes/1
]).
@ -74,6 +75,7 @@ api_spec() ->
paths() ->
[ "/authorization/sources"
, "/authorization/sources/:type"
, "/authorization/sources/:type/status"
, "/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") ->
#{ 'operationId' => move_source
, post =>
@ -250,6 +265,21 @@ source(put, #{bindings := #{type := Type}, body := Body}) when is_map(Body) ->
source(delete, #{bindings := #{type := 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)
when is_atom(Type) ->
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,
HelpFun = fun (M, Name) -> lists:map(MKMap(Name), maps:to_list(M)) end,
case AggregateStatus of
empty_metrics_and_status -> {ok, #{}};
_ -> {ok, #{node_status => HelpFun(StatusMap, status),
empty_metrics_and_status -> {400, #{code => <<"BAD_REQUEST">>,
message => <<"Resource Not Support Status">>}};
_ -> {200, #{node_status => HelpFun(StatusMap, status),
node_metrics => HelpFun(maps:map(Fun, MetricsMap), metrics),
status => AggregateStatus,
metrics => restructure_map(AggregateMetrics)
@ -310,7 +341,8 @@ lookup_from_all_nodes(ResourceId) ->
}
end;
{error, ErrL} ->
{error, error_msg('INTERNAL_ERROR', ErrL)}
{400, #{code => <<"BAD_REQUEST">>,
message => bin_t(io_lib:format("~p", [ErrL]))}}
end.
aggregate_status([]) -> empty_metrics_and_status;
@ -369,9 +401,6 @@ restructure_map(#{counters := #{failed := Failed, matched := Match, success := S
restructure_map(Error) ->
Error.
error_msg(Code, Msg) ->
#{code => Code, message => bin_t(io_lib:format("~p", [Msg]))}.
bin_t(S) when is_list(S) ->
list_to_binary(S).
@ -507,3 +536,28 @@ authz_sources_types(Type) ->
emqx_authz_api_schema:authz_sources_types(Type).
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]),
{ok, 204, _} = request(post, uri(["authorization", "sources"]), ?SOURCE1),
%% Snd = fun ({_, Val}) -> Val end,
%% LookupVal = fun LookupV(List, RestJson) ->
%% case List of
%% [Name] -> Snd(lists:keyfind(Name, 1, RestJson));
%% [Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
%% end
%% end,
%% EqualFun = fun (RList) ->
%% fun ({M, V}) ->
%% ?assertEqual(V,
%% LookupVal([<<"metrics">>, M],
%% RList)
%% )
%% end
%% end,
%% AssertFun =
%% fun (ResultJson) ->
%% {ok, RList} = emqx_json:safe_decode(ResultJson),
%% MetricsList = [{<<"failed">>, 0},
%% {<<"matched">>, 0},
%% {<<"rate">>, 0.0},
%% {<<"rate_last5m">>, 0.0},
%% {<<"rate_max">>, 0.0},
%% {<<"success">>, 0}],
%% lists:map(EqualFun(RList), MetricsList)
%% end,
Snd = fun ({_, Val}) -> Val end,
LookupVal = fun LookupV(List, RestJson) ->
case List of
[Name] -> Snd(lists:keyfind(Name, 1, RestJson));
[Name | NS] -> LookupV(NS, Snd(lists:keyfind(Name, 1, RestJson)))
end
end,
EqualFun = fun (RList) ->
fun ({M, V}) ->
?assertEqual(V,
LookupVal([<<"metrics">>, M],
RList)
)
end
end,
AssertFun =
fun (ResultJson) ->
{ok, RList} = emqx_json:safe_decode(ResultJson),
MetricsList = [{<<"failed">>, 0},
{<<"matched">>, 0},
{<<"rate">>, 0.0},
{<<"rate_last5m">>, 0.0},
{<<"rate_max">>, 0.0},
{<<"success">>, 0}],
lists:map(EqualFun(RList), MetricsList)
end,
{ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []),
Sources = get_sources(Result2),
@ -238,7 +238,8 @@ t_api(_) ->
<<"verify">> => <<"verify_none">>
}}),
{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">>,
<<"ssl">> := #{<<"enable">> := <<"true">>,
<<"cacertfile">> := ?MATCH_CERT,
@ -261,7 +262,8 @@ t_api(_) ->
<<"verify">> => <<"verify_none">>
}}),
{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">>,
<<"ssl">> := #{<<"enable">> := <<"true">>,
<<"cacertfile">> := ?MATCH_CERT,