Просмотр исходного кода

Merge pull request #6078 from savonarola/auth-api-schema-examples

refactor(authn api): add more schema examples
Ilya Averyanov 4 лет назад
Родитель
Сommit
0f8ad29e91
2 измененных файлов с 410 добавлено и 258 удалено
  1. 250 62
      apps/emqx_authn/src/emqx_authn_api.erl
  2. 160 196
      apps/emqx_authn/test/emqx_authn_api_SUITE.erl

+ 250 - 62
apps/emqx_authn/src/emqx_authn_api.erl

@@ -53,7 +53,14 @@
         , listener_authenticator_user/2
         ]).
 
--export([authenticator_examples/0]).
+-export([ authenticator_examples/0
+        , request_move_examples/0
+        , request_import_users_examples/0
+        , request_user_create_examples/0
+        , request_user_update_examples/0
+        , response_user_examples/0
+        , response_users_example/0
+        ]).
 
 api_spec() ->
     emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
@@ -78,13 +85,13 @@ roots() -> [ request_user_create
            , request_move
            , request_import_users
            , response_user
+           , response_users
            ].
 
 fields(request_user_create) ->
     [
-        {user_id, binary()},
-        {password, binary()},
-        {is_superuser, mk(boolean(), #{default => false, nullable => true})}
+        {user_id, binary()}
+        | fields(request_user_update)
     ];
 
 fields(request_user_update) ->
@@ -103,11 +110,21 @@ fields(response_user) ->
     [
         {user_id, binary()},
         {is_superuser, mk(boolean(), #{default => false, nullable => true})}
+    ];
+
+fields(response_users) ->
+    paginated_list_type(ref(response_user));
+
+fields(pagination_meta) ->
+    [
+        {page, non_neg_integer()},
+        {limit, non_neg_integer()},
+        {count, non_neg_integer()}
     ].
 
 schema("/authentication") ->
     #{
-        operationId => authenticators,
+        'operationId' => authenticators,
         get => #{
             tags => [<<"authentication">>, <<"global">>],
             description => <<"List authenticators for global authentication">>,
@@ -120,7 +137,7 @@ schema("/authentication") ->
         post => #{
             tags => [<<"authentication">>, <<"global">>],
             description => <<"Create authenticator for global authentication">>,
-            requestBody => emqx_dashboard_swagger:schema_with_examples(
+            'requestBody' => emqx_dashboard_swagger:schema_with_examples(
                 emqx_authn_schema:authenticator_type(),
                 authenticator_examples()),
             responses => #{
@@ -135,7 +152,7 @@ schema("/authentication") ->
 
 schema("/authentication/:id") ->
     #{
-        operationId => authenticator,
+        'operationId' => authenticator,
         get => #{
             tags => [<<"authentication">>, <<"global">>],
             description => <<"Get authenticator from global authentication chain">>,
@@ -151,7 +168,7 @@ schema("/authentication/:id") ->
             tags => [<<"authentication">>, <<"global">>],
             description => <<"Update authenticator from global authentication chain">>,
             parameters => [{id, mk(binary(), #{in => path, desc => <<"Authenticator ID">>})}],
-            requestBody => emqx_dashboard_swagger:schema_with_examples(
+            'requestBody' => emqx_dashboard_swagger:schema_with_examples(
                 emqx_authn_schema:authenticator_type(),
                 authenticator_examples()
             ),
@@ -177,7 +194,7 @@ schema("/authentication/:id") ->
 
 schema("/listeners/:listener_id/authentication") ->
     #{
-        operationId => listener_authenticators,
+        'operationId' => listener_authenticators,
         get => #{
             tags => [<<"authentication">>, <<"listener">>],
             description => <<"List authenticators for listener authentication">>,
@@ -192,7 +209,7 @@ schema("/listeners/:listener_id/authentication") ->
             tags => [<<"authentication">>, <<"listener">>],
             description => <<"Create authenticator for listener authentication">>,
             parameters => [{listener_id, mk(binary(), #{in => path, desc => <<"Listener ID">>})}],
-            requestBody => emqx_dashboard_swagger:schema_with_examples(
+            'requestBody' => emqx_dashboard_swagger:schema_with_examples(
                 emqx_authn_schema:authenticator_type(),
                 authenticator_examples()
             ),
@@ -208,7 +225,7 @@ schema("/listeners/:listener_id/authentication") ->
 
 schema("/listeners/:listener_id/authentication/:id") ->
     #{
-        operationId => listener_authenticator,
+        'operationId' => listener_authenticator,
         get => #{
             tags => [<<"authentication">>, <<"listener">>],
             description => <<"Get authenticator from listener authentication chain">>,
@@ -230,7 +247,7 @@ schema("/listeners/:listener_id/authentication/:id") ->
                 {listener_id, mk(binary(), #{in => path, desc => <<"Listener ID">>})},
                 {id, mk(binary(), #{in => path, desc => <<"Authenticator ID">>})}
             ],
-            requestBody => emqx_dashboard_swagger:schema_with_examples(
+            'requestBody' => emqx_dashboard_swagger:schema_with_examples(
                 emqx_authn_schema:authenticator_type(),
                 authenticator_examples()),
             responses => #{
@@ -259,12 +276,14 @@ schema("/listeners/:listener_id/authentication/:id") ->
 
 schema("/authentication/:id/move") ->
     #{
-        operationId => authenticator_move,
+        'operationId' => authenticator_move,
         post => #{
             tags => [<<"authentication">>, <<"global">>],
             description => <<"Move authenticator in global authentication chain">>,
             parameters => [{id, mk(binary(), #{in => path, desc => <<"Authenticator ID">>})}],
-            requestBody => ref(request_move),
+            'requestBody' => emqx_dashboard_swagger:schema_with_examples(
+                ref(request_move),
+                request_move_examples()),
             responses => #{
                 204 => <<"Authenticator moved">>,
                 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
@@ -275,7 +294,7 @@ schema("/authentication/:id/move") ->
 
 schema("/listeners/:listener_id/authentication/:id/move") ->
     #{
-        operationId => listener_authenticator_move,
+        'operationId' => listener_authenticator_move,
         post => #{
             tags => [<<"authentication">>, <<"listener">>],
             description => <<"Move authenticator in listener authentication chain">>,
@@ -283,7 +302,9 @@ schema("/listeners/:listener_id/authentication/:id/move") ->
                 {listener_id, mk(binary(), #{in => path, desc => <<"Listener ID">>})},
                 {id, mk(binary(), #{in => path, desc => <<"Authenticator ID">>})}
             ],
-            requestBody => ref(request_move),
+            'requestBody' => emqx_dashboard_swagger:schema_with_examples(
+                ref(request_move),
+                request_move_examples()),
             responses => #{
                 204 => <<"Authenticator moved">>,
                 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
@@ -294,12 +315,14 @@ schema("/listeners/:listener_id/authentication/:id/move") ->
 
 schema("/authentication/:id/import_users") ->
     #{
-        operationId => authenticator_import_users,
+        'operationId' => authenticator_import_users,
         post => #{
             tags => [<<"authentication">>, <<"global">>],
             description => <<"Import users into authenticator in global authentication chain">>,
             parameters => [{id, mk(binary(), #{in => path, desc => <<"Authenticator ID">>})}],
-            requestBody => ref(request_import_users),
+            'requestBody' => emqx_dashboard_swagger:schema_with_examples(
+                ref(request_import_users),
+                request_import_users_examples()),
             responses => #{
                 204 => <<"Users imported">>,
                 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
@@ -310,7 +333,7 @@ schema("/authentication/:id/import_users") ->
 
 schema("/listeners/:listener_id/authentication/:id/import_users") ->
     #{
-        operationId => listener_authenticator_import_users,
+        'operationId' => listener_authenticator_import_users,
         post => #{
             tags => [<<"authentication">>, <<"listener">>],
             description => <<"Import users into authenticator in listener authentication chain">>,
@@ -318,7 +341,9 @@ schema("/listeners/:listener_id/authentication/:id/import_users") ->
                 {listener_id, mk(binary(), #{in => path, desc => <<"Listener ID">>})},
                 {id, mk(binary(), #{in => path, desc => <<"Authenticator ID">>})}
             ],
-            requestBody => ref(request_import_users),
+            'requestBody' => emqx_dashboard_swagger:schema_with_examples(
+                ref(request_import_users),
+                request_import_users_examples()),
             responses => #{
                 204 => <<"Users imported">>,
                 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
@@ -329,14 +354,18 @@ schema("/listeners/:listener_id/authentication/:id/import_users") ->
 
 schema("/authentication/:id/users") ->
     #{
-        operationId => authenticator_users,
+        'operationId' => authenticator_users,
         post => #{
             tags => [<<"authentication">>, <<"global">>],
             description => <<"Create users for authenticator in global authentication chain">>,
             parameters => [{id, mk(binary(), #{in => path, desc => <<"Authenticator ID">>})}],
-            requestBody => ref(request_user_create),
+            'requestBody' => emqx_dashboard_swagger:schema_with_examples(
+                ref(request_user_create),
+                request_user_create_examples()),
             responses => #{
-                201 => ref(response_user),
+                201 => emqx_dashboard_swagger:schema_with_examples(
+                    ref(response_user),
+                    response_user_examples()),
                 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
                 404 => error_codes([?NOT_FOUND], <<"Not Found">>)
             }
@@ -350,7 +379,9 @@ schema("/authentication/:id/users") ->
                 {limit, mk(integer(), #{in => query, desc => <<"Page Limit">>, nullable => true})}
             ],
             responses => #{
-                200 => mk(hoconsc:array(ref(response_user)), #{}),
+                200 => emqx_dashboard_swagger:schema_with_example(
+                    ref(response_users),
+                    response_users_example()),
                 404 => error_codes([?NOT_FOUND], <<"Not Found">>)
             }
 
@@ -359,7 +390,7 @@ schema("/authentication/:id/users") ->
 
 schema("/listeners/:listener_id/authentication/:id/users") ->
     #{
-        operationId => listener_authenticator_users,
+        'operationId' => listener_authenticator_users,
         post => #{
             tags => [<<"authentication">>, <<"listener">>],
             description => <<"Create users for authenticator in global authentication chain">>,
@@ -367,9 +398,13 @@ schema("/listeners/:listener_id/authentication/:id/users") ->
                 {listener_id, mk(binary(), #{in => path, desc => <<"Listener ID">>})},
                 {id, mk(binary(), #{in => path, desc => <<"Authenticator ID">>})}
             ],
-            requestBody => ref(request_user_create),
+            'requestBody' => emqx_dashboard_swagger:schema_with_examples(
+                ref(request_user_create),
+                request_user_create_examples()),
             responses => #{
-                201 => ref(response_user),
+                201 => emqx_dashboard_swagger:schema_with_examples(
+                    ref(response_user),
+                    response_user_examples()),
                 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
                 404 => error_codes([?NOT_FOUND], <<"Not Found">>)
             }
@@ -384,7 +419,9 @@ schema("/listeners/:listener_id/authentication/:id/users") ->
                 {limit, mk(integer(), #{in => query, desc => <<"Page Limit">>, nullable => true})}
             ],
             responses => #{
-                200 => mk(hoconsc:array(ref(response_user)), #{}),
+                200 => emqx_dashboard_swagger:schema_with_example(
+                    ref(response_users),
+                    response_users_example()),
                 404 => error_codes([?NOT_FOUND], <<"Not Found">>)
             }
 
@@ -393,7 +430,7 @@ schema("/listeners/:listener_id/authentication/:id/users") ->
 
 schema("/authentication/:id/users/:user_id") ->
     #{
-        operationId => authenticator_user,
+        'operationId' => authenticator_user,
         get => #{
             tags => [<<"authentication">>, <<"global">>],
             description => <<"Get user from authenticator in global authentication chain">>,
@@ -402,7 +439,9 @@ schema("/authentication/:id/users/:user_id") ->
                 {user_id, mk(binary(), #{in => path, desc => <<"User ID">>})}
             ],
             responses => #{
-                200 => ref(response_user),
+                200 => emqx_dashboard_swagger:schema_with_examples(
+                    ref(response_user),
+                    response_user_examples()),
                 404 => error_codes([?NOT_FOUND], <<"Not Found">>)
             }
         },
@@ -413,9 +452,13 @@ schema("/authentication/:id/users/:user_id") ->
                 {id, mk(binary(), #{in => path, desc => <<"Authenticator ID">>})},
                 {user_id, mk(binary(), #{in => path, desc => <<"User ID">>})}
             ],
-            requestBody => ref(request_user_update),
+            'requestBody' => emqx_dashboard_swagger:schema_with_examples(
+                ref(request_user_update),
+                request_user_update_examples()),
             responses => #{
-                200 => mk(hoconsc:array(ref(response_user)), #{}),
+                200 => emqx_dashboard_swagger:schema_with_example(
+                    ref(response_user),
+                    response_user_examples()),
                 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
                 404 => error_codes([?NOT_FOUND], <<"Not Found">>)
             }
@@ -436,7 +479,7 @@ schema("/authentication/:id/users/:user_id") ->
 
 schema("/listeners/:listener_id/authentication/:id/users/:user_id") ->
     #{
-        operationId => listener_authenticator_user,
+        'operationId' => listener_authenticator_user,
         get => #{
             tags => [<<"authentication">>, <<"listener">>],
             description => <<"Get user from authenticator in listener authentication chain">>,
@@ -446,7 +489,9 @@ schema("/listeners/:listener_id/authentication/:id/users/:user_id") ->
                 {user_id, mk(binary(), #{in => path, desc => <<"User ID">>})}
             ],
             responses => #{
-                200 => ref(response_user),
+                200 => emqx_dashboard_swagger:schema_with_example(
+                    ref(response_user),
+                    response_user_examples()),
                 404 => error_codes([?NOT_FOUND], <<"Not Found">>)
             }
         },
@@ -458,9 +503,13 @@ schema("/listeners/:listener_id/authentication/:id/users/:user_id") ->
                 {id, mk(binary(), #{in => path, desc => <<"Authenticator ID">>})},
                 {user_id, mk(binary(), #{in => path, desc => <<"User ID">>})}
             ],
-            requestBody => ref(request_user_update),
+            'requestBody' => emqx_dashboard_swagger:schema_with_example(
+                ref(request_user_update),
+                request_user_update_examples()),
             responses => #{
-                200 => mk(hoconsc:array(ref(response_user)), #{}),
+                200 => emqx_dashboard_swagger:schema_with_example(
+                    ref(response_user),
+                    response_user_examples()),
                 400 => error_codes([?BAD_REQUEST], <<"Bad Request">>),
                 404 => error_codes([?NOT_FOUND], <<"Not Found">>)
             }
@@ -516,7 +565,9 @@ listener_authenticator(get, #{bindings := #{listener_id := ListenerID, id := Aut
                         list_authenticator([listeners, Type, Name, authentication],
                                        AuthenticatorID)
                   end);
-listener_authenticator(put, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, body := Config}) ->
+listener_authenticator(put,
+                       #{bindings := #{listener_id := ListenerID, id := AuthenticatorID},
+                         body := Config}) ->
     with_listener(ListenerID,
                   fun(Type, Name, ChainName) ->
                         update_authenticator([listeners, Type, Name, authentication],
@@ -524,7 +575,8 @@ listener_authenticator(put, #{bindings := #{listener_id := ListenerID, id := Aut
                                              AuthenticatorID,
                                              Config)
                   end);
-listener_authenticator(delete, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}}) ->
+listener_authenticator(delete,
+                       #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}}) ->
     with_listener(ListenerID,
                   fun(Type, Name, ChainName) ->
                         delete_authenticator([listeners, Type, Name, authentication],
@@ -532,12 +584,16 @@ listener_authenticator(delete, #{bindings := #{listener_id := ListenerID, id :=
                                              AuthenticatorID)
                   end).
 
-authenticator_move(post, #{bindings := #{id := AuthenticatorID}, body := #{<<"position">> := Position}}) ->
+authenticator_move(post,
+                   #{bindings := #{id := AuthenticatorID},
+                     body := #{<<"position">> := Position}}) ->
     move_authenitcator([authentication], ?GLOBAL, AuthenticatorID, Position);
 authenticator_move(post, #{bindings := #{id := _}, body := _}) ->
     serialize_error({missing_parameter, position}).
 
-listener_authenticator_move(post, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, body := #{<<"position">> := Position}}) ->
+listener_authenticator_move(post,
+                            #{bindings := #{listener_id := ListenerID, id := AuthenticatorID},
+                              body := #{<<"position">> := Position}}) ->
     with_listener(ListenerID,
                   fun(Type, Name, ChainName) ->
                         move_authenitcator([listeners, Type, Name, authentication],
@@ -548,22 +604,28 @@ listener_authenticator_move(post, #{bindings := #{listener_id := ListenerID, id
 listener_authenticator_move(post, #{bindings := #{listener_id := _, id := _}, body := _}) ->
     serialize_error({missing_parameter, position}).
 
-authenticator_import_users(post, #{bindings := #{id := AuthenticatorID}, body := #{<<"filename">> := Filename}}) ->
-    case ?AUTHN:import_users(?GLOBAL, AuthenticatorID, Filename) of
+authenticator_import_users(post,
+                           #{bindings := #{id := AuthenticatorID},
+                             body := #{<<"filename">> := Filename}}) ->
+    case emqx_authentication:import_users(?GLOBAL, AuthenticatorID, Filename) of
         ok -> {204};
         {error, Reason} -> serialize_error(Reason)
     end;
 authenticator_import_users(post, #{bindings := #{id := _}, body := _}) ->
     serialize_error({missing_parameter, filename}).
 
-listener_authenticator_import_users(post, #{bindings := #{listener_id := ListenerID, id := AuthenticatorID}, body := #{<<"filename">> := Filename}}) ->
-    with_chain(ListenerID,
-                    fun(ChainName) ->
-                        case ?AUTHN:import_users(ChainName, AuthenticatorID, Filename) of
-                            ok -> {204};
-                            {error, Reason} -> serialize_error(Reason)
-                        end
-                    end);
+listener_authenticator_import_users(
+  post,
+  #{bindings := #{listener_id := ListenerID, id := AuthenticatorID},
+    body := #{<<"filename">> := Filename}}) ->
+    with_chain(
+      ListenerID,
+      fun(ChainName) ->
+              case emqx_authentication:import_users(ChainName, AuthenticatorID, Filename) of
+                  ok -> {204};
+                  {error, Reason} -> serialize_error(Reason)
+              end
+      end);
 listener_authenticator_import_users(post, #{bindings := #{listener_id := _, id := _}, body := _}) ->
     serialize_error({missing_parameter, filename}).
 
@@ -644,7 +706,7 @@ find_listener(ListenerID) ->
     end.
 
 with_chain(ListenerID, Fun) ->
-    {ok, ChainNames} = ?AUTHN:list_chain_names(),
+    {ok, ChainNames} = emqx_authentication:list_chain_names(),
     ListenerChainName =
         [ Name || Name <- ChainNames, atom_to_binary(Name) =:= ListenerID ],
     case ListenerChainName of
@@ -656,7 +718,7 @@ with_chain(ListenerID, Fun) ->
 
 create_authenticator(ConfKeyPath, ChainName, Config) ->
     case update_config(ConfKeyPath, {create_authenticator, ChainName, Config}) of
-        {ok, #{post_config_update := #{?AUTHN := #{id := ID}},
+        {ok, #{post_config_update := #{emqx_authentication := #{id := ID}},
             raw_config := AuthenticatorsConfig}} ->
             {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig),
             {200, maps:put(id, ID, convert_certs(fill_defaults(AuthenticatorConfig)))};
@@ -666,7 +728,10 @@ create_authenticator(ConfKeyPath, ChainName, Config) ->
 
 list_authenticators(ConfKeyPath) ->
     AuthenticatorsConfig = get_raw_config_with_defaults(ConfKeyPath),
-    NAuthenticators = [maps:put(id, ?AUTHN:authenticator_id(AuthenticatorConfig), convert_certs(AuthenticatorConfig))
+    NAuthenticators = [ maps:put(
+                          id,
+                          emqx_authentication:authenticator_id(AuthenticatorConfig),
+                          convert_certs(AuthenticatorConfig))
                         || AuthenticatorConfig <- AuthenticatorsConfig],
     {200, NAuthenticators}.
 
@@ -681,7 +746,7 @@ list_authenticator(ConfKeyPath, AuthenticatorID) ->
 
 update_authenticator(ConfKeyPath, ChainName, AuthenticatorID, Config) ->
     case update_config(ConfKeyPath, {update_authenticator, ChainName, AuthenticatorID, Config}) of
-        {ok, #{post_config_update := #{?AUTHN := #{id := ID}},
+        {ok, #{post_config_update := #{emqx_authentication := #{id := ID}},
                raw_config := AuthenticatorsConfig}} ->
             {ok, AuthenticatorConfig} = find_config(ID, AuthenticatorsConfig),
             {200, maps:put(id, ID, convert_certs(fill_defaults(AuthenticatorConfig)))};
@@ -700,7 +765,9 @@ delete_authenticator(ConfKeyPath, ChainName, AuthenticatorID) ->
 move_authenitcator(ConfKeyPath, ChainName, AuthenticatorID, Position) ->
     case parse_position(Position) of
         {ok, NPosition} ->
-            case update_config(ConfKeyPath, {move_authenticator, ChainName, AuthenticatorID, NPosition}) of
+            case update_config(
+                   ConfKeyPath,
+                   {move_authenticator, ChainName, AuthenticatorID, NPosition}) of
                 {ok, _} ->
                     {204};
                 {error, {_, _, Reason}} ->
@@ -710,9 +777,11 @@ move_authenitcator(ConfKeyPath, ChainName, AuthenticatorID, Position) ->
             serialize_error(Reason)
     end.
 
-add_user(ChainName, AuthenticatorID, #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) ->
+add_user(ChainName,
+         AuthenticatorID,
+         #{<<"user_id">> := UserID, <<"password">> := Password} = UserInfo) ->
     IsSuperuser = maps:get(<<"is_superuser">>, UserInfo, false),
-    case ?AUTHN:add_user(ChainName, AuthenticatorID, #{ user_id => UserID
+    case emqx_authentication:add_user(ChainName, AuthenticatorID, #{ user_id => UserID
                                                       , password => Password
                                                       , is_superuser => IsSuperuser}) of
         {ok, User} ->
@@ -730,7 +799,7 @@ update_user(ChainName, AuthenticatorID, UserID, UserInfo) ->
         true ->
             serialize_error({missing_parameter, password});
         false ->
-            case ?AUTHN:update_user(ChainName, AuthenticatorID, UserID, UserInfo) of
+            case emqx_authentication:update_user(ChainName, AuthenticatorID, UserID, UserInfo) of
                 {ok, User} ->
                     {200, User};
                 {error, Reason} ->
@@ -739,7 +808,7 @@ update_user(ChainName, AuthenticatorID, UserID, UserInfo) ->
     end.
 
 find_user(ChainName, AuthenticatorID, UserID) ->
-    case ?AUTHN:lookup_user(ChainName, AuthenticatorID, UserID) of
+    case emqx_authentication:lookup_user(ChainName, AuthenticatorID, UserID) of
         {ok, User} ->
             {200, User};
         {error, Reason} ->
@@ -747,7 +816,7 @@ find_user(ChainName, AuthenticatorID, UserID) ->
     end.
 
 delete_user(ChainName, AuthenticatorID, UserID) ->
-    case ?AUTHN:delete_user(ChainName, AuthenticatorID, UserID) of
+    case emqx_authentication:delete_user(ChainName, AuthenticatorID, UserID) of
         ok ->
             {204};
         {error, Reason} ->
@@ -755,7 +824,7 @@ delete_user(ChainName, AuthenticatorID, UserID) ->
     end.
 
 list_users(ChainName, AuthenticatorID, PageParams) ->
-    case ?AUTHN:list_users(ChainName, AuthenticatorID, PageParams) of
+    case emqx_authentication:list_users(ChainName, AuthenticatorID, PageParams) of
         {ok, Users} ->
             {200, Users};
         {error, Reason} ->
@@ -771,7 +840,11 @@ get_raw_config_with_defaults(ConfKeyPath) ->
     ensure_list(fill_defaults(RawConfig)).
 
 find_config(AuthenticatorID, AuthenticatorsConfig) ->
-    case [AC || AC <- ensure_list(AuthenticatorsConfig), AuthenticatorID =:= ?AUTHN:authenticator_id(AC)] of
+    MatchingACs
+        = [AC
+           || AC <- ensure_list(AuthenticatorsConfig),
+              AuthenticatorID =:= emqx_authentication:authenticator_id(AC)],
+    case MatchingACs of
         [] -> {error, {not_found, {authenticator, AuthenticatorID}}};
         [AuthenticatorConfig] -> {ok, AuthenticatorConfig}
     end.
@@ -860,6 +933,12 @@ ensure_list(L) when is_list(L) -> L.
 
 binfmt(Fmt, Args) -> iolist_to_binary(io_lib:format(Fmt, Args)).
 
+paginated_list_type(Type) ->
+    [
+        {data, hoconsc:array(Type)},
+        {meta, ref(pagination_meta)}
+    ].
+
 authenticator_array_example() ->
     [Config || #{value := Config} <- maps:values(authenticator_examples())].
 
@@ -941,3 +1020,112 @@ authenticator_examples() ->
             }
         }
     }.
+
+request_user_create_examples() ->
+    #{
+        regular_user => #{
+            summary => <<"Regular user">>,
+            value => #{
+                user_id => <<"user1">>,
+                password => <<"secret">>
+            }
+        },
+        super_user => #{
+            summary => <<"Superuser">>,
+            value => #{
+                user_id => <<"user2">>,
+                password => <<"secret">>,
+                is_superuser => true
+            }
+        }
+    }.
+
+request_user_update_examples() ->
+    #{
+        regular_user => #{
+            summary => <<"Update regular user">>,
+            value => #{
+                password => <<"newsecret">>
+            }
+        },
+        super_user => #{
+            summary => <<"Update user and promote to superuser">>,
+            value => #{
+                password => <<"newsecret">>,
+                is_superuser => true
+            }
+        }
+    }.
+
+request_move_examples() ->
+    #{
+        move_to_top => #{
+            summary => <<"Move authenticator to the beginning of the chain">>,
+            value => #{
+                position => <<"top">>
+            }
+        },
+        move_to_bottom => #{
+            summary => <<"Move authenticator to the end of the chain">>,
+            value => #{
+                position => <<"bottom">>
+            }
+        },
+        'move_before_password-based:built-in-database' => #{
+            summary => <<"Move authenticator to the position preceding some other authenticator">>,
+            value => #{
+                position => <<"before:password-based:built-in-database">>
+            }
+        }
+    }.
+
+request_import_users_examples() ->
+    #{
+        import_csv => #{
+            summary => <<"Import users from CSV file">>,
+            value => #{
+                filename => <<"/path/to/user/data.csv">>
+            }
+        },
+        import_json => #{
+            summary => <<"Import users from JSON file">>,
+            value => #{
+                filename => <<"/path/to/user/data.json">>
+            }
+        }
+    }.
+
+response_user_examples() ->
+    #{
+        regular_user => #{
+            summary => <<"Regular user">>,
+            value => #{
+                user_id => <<"user1">>
+            }
+        },
+        super_user => #{
+            summary => <<"Superuser">>,
+            value => #{
+                user_id => <<"user2">>,
+                is_superuser => true
+            }
+        }
+    }.
+
+response_users_example() ->
+    #{
+        data => [
+            #{
+                user_id => <<"user1">>
+            },
+            #{
+                user_id => <<"user2">>,
+                is_superuser => true
+            }
+        ],
+        meta => #{
+            page => 0,
+            limit => 20,
+            count => 300
+        }
+    }.

+ 160 - 196
apps/emqx_authn/test/emqx_authn_api_SUITE.erl

@@ -49,7 +49,9 @@ init_per_testcase(_, Config) ->
     Config.
 
 init_per_suite(Config) ->
-    ok = emqx_common_test_helpers:start_apps([emqx_authn, emqx_dashboard], fun set_special_configs/1),
+    ok = emqx_common_test_helpers:start_apps(
+           [emqx_authn, emqx_dashboard],
+           fun set_special_configs/1),
     Config.
 
 end_per_suite(_Config) ->
@@ -118,309 +120,275 @@ test_authenticators(PathPrefix) ->
 
     ValidConfig = emqx_authn_test_lib:http_example(),
     {ok, 200, _} = request(
-                    post,
-                    uri(PathPrefix ++ ["authentication"]),
-                    ValidConfig),
+                     post,
+                     uri(PathPrefix ++ ["authentication"]),
+                     ValidConfig),
 
     InvalidConfig = ValidConfig#{method => <<"delete">>},
     {ok, 400, _} = request(
-                    post,
-                    uri(PathPrefix ++ ["authentication"]),
-                    InvalidConfig),
+                     post,
+                     uri(PathPrefix ++ ["authentication"]),
+                     InvalidConfig),
 
     ?assertAuthenticatorsMatch(
-        [#{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>}],
-        PathPrefix ++ ["authentication"]).
+       [#{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>}],
+       PathPrefix ++ ["authentication"]).
 
 test_authenticator(PathPrefix) ->
     ValidConfig0 = emqx_authn_test_lib:http_example(),
 
     {ok, 200, _} = request(
-                    post,
-                    uri(PathPrefix ++ ["authentication"]),
-                    ValidConfig0),
+                     post,
+                     uri(PathPrefix ++ ["authentication"]),
+                     ValidConfig0),
 
     {ok, 200, _} = request(
-                    get,
-                    uri(PathPrefix ++ ["authentication", "password-based:http"])),
+                     get,
+                     uri(PathPrefix ++ ["authentication", "password-based:http"])),
 
     {ok, 404, _} = request(
-                    get,
-                    uri(PathPrefix ++ ["authentication", "password-based:redis"])),
+                     get,
+                     uri(PathPrefix ++ ["authentication", "password-based:redis"])),
 
 
     {ok, 404, _} = request(
-                    put,
-                    uri(PathPrefix ++ ["authentication", "password-based:built-in-database"]),
-                    emqx_authn_test_lib:built_in_database_example()),
+                     put,
+                     uri(PathPrefix ++ ["authentication", "password-based:built-in-database"]),
+                     emqx_authn_test_lib:built_in_database_example()),
 
     InvalidConfig0 = ValidConfig0#{method => <<"delete">>},
     {ok, 400, _} = request(
-                    put,
-                    uri(PathPrefix ++ ["authentication", "password-based:http"]),
-                    InvalidConfig0),
+                     put,
+                     uri(PathPrefix ++ ["authentication", "password-based:http"]),
+                     InvalidConfig0),
 
     ValidConfig1 = ValidConfig0#{pool_size => 9},
     {ok, 200, _} = request(
-                    put,
-                    uri(PathPrefix ++ ["authentication", "password-based:http"]),
-                    ValidConfig1),
+                     put,
+                     uri(PathPrefix ++ ["authentication", "password-based:http"]),
+                     ValidConfig1),
 
     {ok, 404, _} = request(
-                    delete,
-                    uri(PathPrefix ++ ["authentication", "password-based:redis"])),
+                     delete,
+                     uri(PathPrefix ++ ["authentication", "password-based:redis"])),
 
     {ok, 204, _} = request(
-                    delete,
-                    uri(PathPrefix ++ ["authentication", "password-based:http"])),
+                     delete,
+                     uri(PathPrefix ++ ["authentication", "password-based:http"])),
 
     ?assertAuthenticatorsMatch([], PathPrefix ++ ["authentication"]).
 
 test_authenticator_users(PathPrefix) ->
+    UsersUri = uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]),
+
     {ok, 200, _} = request(
-                    post,
-                    uri(PathPrefix ++ ["authentication"]),
-                    emqx_authn_test_lib:built_in_database_example()),
+                     post,
+                     uri(PathPrefix ++ ["authentication"]),
+                     emqx_authn_test_lib:built_in_database_example()),
 
     InvalidUsers = [
-        #{clientid => <<"u1">>, password => <<"p1">>},
-        #{user_id => <<"u2">>},
-        #{user_id => <<"u3">>, password => <<"p3">>, foobar => <<"foobar">>}],
+                    #{clientid => <<"u1">>, password => <<"p1">>},
+                    #{user_id => <<"u2">>},
+                    #{user_id => <<"u3">>, password => <<"p3">>, foobar => <<"foobar">>}],
 
     lists:foreach(
-        fun(User) ->
-            {ok, 400, _} = request(
-                            post,
-                            uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]),
-                            User)
-        end,
-        InvalidUsers),
+      fun(User) -> {ok, 400, _} = request(post, UsersUri, User) end,
+      InvalidUsers),
 
 
     ValidUsers = [
-        #{user_id => <<"u1">>, password => <<"p1">>},
-        #{user_id => <<"u2">>, password => <<"p2">>, is_superuser => true},
-        #{user_id => <<"u3">>, password => <<"p3">>}],
+                  #{user_id => <<"u1">>, password => <<"p1">>},
+                  #{user_id => <<"u2">>, password => <<"p2">>, is_superuser => true},
+                  #{user_id => <<"u3">>, password => <<"p3">>}],
 
     lists:foreach(
-        fun(User) ->
-            {ok, 201, _} = request(
-                                post,
-                                uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]),
-                                User)
-        end,
-        ValidUsers),
-
-    {ok, 200, Page1Data} =
-        request(
-            get,
-            uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]) ++ "?page=1&limit=2"),
-
-    Page1Users = response_data(Page1Data),
-
-    {ok, 200, Page2Data} =
-        request(
-            get,
-            uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]) ++ "?page=2&limit=2"),
-
-    Page2Users = response_data(Page2Data),
+      fun(User) ->
+          {ok, 201, UserData} = request(post, UsersUri, User),
+          CreatedUser = jiffy:decode(UserData, [return_maps]),
+          ?assertMatch(#{<<"user_id">> := _}, CreatedUser)
+      end,
+      ValidUsers),
+
+    {ok, 200, Page1Data} = request(get, UsersUri ++ "?page=1&limit=2"),
+
+    #{<<"data">> := Page1Users,
+      <<"meta">> :=
+      #{<<"page">> := 1,
+        <<"limit">> := 2,
+        <<"count">> := 3}} =
+    jiffy:decode(Page1Data, [return_maps]),
+
+    {ok, 200, Page2Data} = request(get, UsersUri ++ "?page=2&limit=2"),
+
+    #{<<"data">> := Page2Users,
+      <<"meta">> :=
+      #{<<"page">> := 2,
+        <<"limit">> := 2,
+        <<"count">> := 3}} = jiffy:decode(Page2Data, [return_maps]),
 
     ?assertEqual(2, length(Page1Users)),
     ?assertEqual(1, length(Page2Users)),
 
     ?assertEqual(
-        [<<"u1">>, <<"u2">>, <<"u3">>],
-        lists:usort([ UserId || #{<<"user_id">> := UserId} <- Page1Users ++ Page2Users])).
+       [<<"u1">>, <<"u2">>, <<"u3">>],
+       lists:usort([ UserId || #{<<"user_id">> := UserId} <- Page1Users ++ Page2Users])).
 
 test_authenticator_user(PathPrefix) ->
+    UsersUri = uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]),
+
     {ok, 200, _} = request(
-                        post,
-                        uri(PathPrefix ++ ["authentication"]),
-                        emqx_authn_test_lib:built_in_database_example()),
+                     post,
+                     uri(PathPrefix ++ ["authentication"]),
+                     emqx_authn_test_lib:built_in_database_example()),
 
     User = #{user_id => <<"u1">>, password => <<"p1">>},
-    {ok, 201, _} = request(
-                        post,
-                        uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]),
-                        User),
+    {ok, 201, _} = request(post, UsersUri, User),
 
-    {ok, 404, _} = request(
-                        get,
-                        uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users", "u123"])),
+    {ok, 404, _} = request(get, UsersUri ++ "/u123"),
 
-    {ok, 409, _} = request(
-                        post,
-                        uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users"]),
-                        User),
+    {ok, 409, _} = request(post, UsersUri, User),
 
-    {ok, 200, UserData} = request(
-                            get,
-                            uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users", "u1"])),
+    {ok, 200, UserData} = request(get, UsersUri ++ "/u1"),
 
     FetchedUser = jiffy:decode(UserData, [return_maps]),
     ?assertMatch(#{<<"user_id">> := <<"u1">>}, FetchedUser),
     ?assertNotMatch(#{<<"password">> := _}, FetchedUser),
 
     ValidUserUpdates = [
-        #{password => <<"p1">>},
-        #{password => <<"p1">>, is_superuser => true}],
+                        #{password => <<"p1">>},
+                        #{password => <<"p1">>, is_superuser => true}],
 
     lists:foreach(
-        fun(UserUpdate) ->
-            {ok, 200, _} = request(
-                            put,
-                            uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users", "u1"]),
-                            UserUpdate)
-        end,
-        ValidUserUpdates),
+      fun(UserUpdate) -> {ok, 200, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
+      ValidUserUpdates),
 
     InvalidUserUpdates = [
-        #{user_id => <<"u1">>, password => <<"p1">>},
-        #{is_superuser => true}],
+                          #{user_id => <<"u1">>, password => <<"p1">>},
+                          #{is_superuser => true}],
 
     lists:foreach(
-        fun(UserUpdate) ->
-            {ok, 400, _} = request(
-                            put,
-                            uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users", "u1"]),
-                            UserUpdate)
-        end,
-        InvalidUserUpdates),
-
-    {ok, 404, _} = request(
-                    delete,
-                    uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users", "u123"])),
+      fun(UserUpdate) -> {ok, 400, _} = request(put, UsersUri ++ "/u1", UserUpdate) end,
+      InvalidUserUpdates),
 
-    {ok, 204, _} = request(
-                    delete,
-                    uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "users", "u1"])).
+    {ok, 404, _} = request(delete, UsersUri ++ "/u123"),
+    {ok, 204, _} = request(delete, UsersUri ++ "/u1").
 
 test_authenticator_move(PathPrefix) ->
     AuthenticatorConfs = [
-        emqx_authn_test_lib:http_example(),
-        emqx_authn_test_lib:jwt_example(),
-        emqx_authn_test_lib:built_in_database_example()
-    ],
+                          emqx_authn_test_lib:http_example(),
+                          emqx_authn_test_lib:jwt_example(),
+                          emqx_authn_test_lib:built_in_database_example()
+                         ],
 
     lists:foreach(
-        fun(Conf) ->
-            {ok, 200, _} = request(
-                            post,
-                            uri(PathPrefix ++ ["authentication"]),
-                            Conf)
-        end,
-        AuthenticatorConfs),
+      fun(Conf) ->
+              {ok, 200, _} = request(
+                               post,
+                               uri(PathPrefix ++ ["authentication"]),
+                               Conf)
+      end,
+      AuthenticatorConfs),
 
     ?assertAuthenticatorsMatch(
-        [
-            #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>},
-            #{<<"mechanism">> := <<"jwt">>},
-            #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"built-in-database">>}
-        ],
-        PathPrefix ++ ["authentication"]),
+       [
+        #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>},
+        #{<<"mechanism">> := <<"jwt">>},
+        #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"built-in-database">>}
+       ],
+       PathPrefix ++ ["authentication"]),
 
     % Invalid moves
 
     {ok, 400, _} = request(
-                    post,
-                    uri(PathPrefix ++ ["authentication", "jwt", "move"]),
-                    #{position => <<"up">>}),
+                     post,
+                     uri(PathPrefix ++ ["authentication", "jwt", "move"]),
+                     #{position => <<"up">>}),
 
     {ok, 400, _} = request(
-                    post,
-                    uri(PathPrefix ++ ["authentication", "jwt", "move"]),
-                    #{}),
+                     post,
+                     uri(PathPrefix ++ ["authentication", "jwt", "move"]),
+                     #{}),
 
     {ok, 404, _} = request(
-                    post,
-                    uri(PathPrefix ++ ["authentication", "jwt", "move"]),
-                    #{position => <<"before:invalid">>}),
+                     post,
+                     uri(PathPrefix ++ ["authentication", "jwt", "move"]),
+                     #{position => <<"before:invalid">>}),
 
     {ok, 404, _} = request(
-                    post,
-                    uri(PathPrefix ++ ["authentication", "jwt", "move"]),
-                    #{position => <<"before:password-based:redis">>}),
+                     post,
+                     uri(PathPrefix ++ ["authentication", "jwt", "move"]),
+                     #{position => <<"before:password-based:redis">>}),
 
     {ok, 404, _} = request(
-                    post,
-                    uri(PathPrefix ++ ["authentication", "jwt", "move"]),
-                    #{position => <<"before:password-based:redis">>}),
+                     post,
+                     uri(PathPrefix ++ ["authentication", "jwt", "move"]),
+                     #{position => <<"before:password-based:redis">>}),
 
     % Valid moves
 
     {ok, 204, _} = request(
-                    post,
-                    uri(PathPrefix ++ ["authentication", "jwt", "move"]),
-                    #{position => <<"top">>}),
+                     post,
+                     uri(PathPrefix ++ ["authentication", "jwt", "move"]),
+                     #{position => <<"top">>}),
 
     ?assertAuthenticatorsMatch(
-        [
-            #{<<"mechanism">> := <<"jwt">>},
-            #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>},
-            #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"built-in-database">>}
-        ],
-        PathPrefix ++ ["authentication"]),
+       [
+        #{<<"mechanism">> := <<"jwt">>},
+        #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>},
+        #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"built-in-database">>}
+       ],
+       PathPrefix ++ ["authentication"]),
 
     {ok, 204, _} = request(
-                    post,
-                    uri(PathPrefix ++ ["authentication", "jwt", "move"]),
-                    #{position => <<"bottom">>}),
+                     post,
+                     uri(PathPrefix ++ ["authentication", "jwt", "move"]),
+                     #{position => <<"bottom">>}),
 
     ?assertAuthenticatorsMatch(
-        [
-            #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>},
-            #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"built-in-database">>},
-            #{<<"mechanism">> := <<"jwt">>}
-        ],
-        PathPrefix ++ ["authentication"]),
+       [
+        #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>},
+        #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"built-in-database">>},
+        #{<<"mechanism">> := <<"jwt">>}
+       ],
+       PathPrefix ++ ["authentication"]),
 
     {ok, 204, _} = request(
-                    post,
-                    uri(PathPrefix ++ ["authentication", "jwt", "move"]),
-                    #{position => <<"before:password-based:built-in-database">>}),
+                     post,
+                     uri(PathPrefix ++ ["authentication", "jwt", "move"]),
+                     #{position => <<"before:password-based:built-in-database">>}),
 
     ?assertAuthenticatorsMatch(
-        [
-            #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>},
-            #{<<"mechanism">> := <<"jwt">>},
-            #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"built-in-database">>}
-        ],
-        PathPrefix ++ ["authentication"]).
+       [
+        #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"http">>},
+        #{<<"mechanism">> := <<"jwt">>},
+        #{<<"mechanism">> := <<"password-based">>, <<"backend">> := <<"built-in-database">>}
+       ],
+       PathPrefix ++ ["authentication"]).
 
 test_authenticator_import_users(PathPrefix) ->
+    ImportUri = uri(
+                  PathPrefix ++
+                  ["authentication", "password-based:built-in-database", "import_users"]),
+
+
     {ok, 200, _} = request(
-                        post,
-                        uri(PathPrefix ++ ["authentication"]),
-                        emqx_authn_test_lib:built_in_database_example()),
+                     post,
+                     uri(PathPrefix ++ ["authentication"]),
+                     emqx_authn_test_lib:built_in_database_example()),
 
-    {ok, 400, _} = request(
-                        post,
-                        uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "import_users"]),
-                        #{}),
+    {ok, 400, _} = request(post, ImportUri, #{}),
 
-    {ok, 400, _} = request(
-                        post,
-                        uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "import_users"]),
-                        #{filename => <<"/etc/passwd">>}),
+    {ok, 400, _} = request(post, ImportUri, #{filename => <<"/etc/passwd">>}),
 
-    {ok, 400, _} = request(
-                        post,
-                        uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "import_users"]),
-                        #{filename => <<"/not_exists.csv">>}),
+    {ok, 400, _} = request(post, ImportUri, #{filename => <<"/not_exists.csv">>}),
 
     Dir = code:lib_dir(emqx_authn, test),
     JSONFileName = filename:join([Dir, <<"data/user-credentials.json">>]),
     CSVFileName = filename:join([Dir, <<"data/user-credentials.csv">>]),
 
-    {ok, 204, _} = request(
-                        post,
-                        uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "import_users"]),
-                        #{filename => JSONFileName}),
+    {ok, 204, _} = request(post, ImportUri, #{filename => JSONFileName}),
 
-    {ok, 204, _} = request(
-                        post,
-                        uri(PathPrefix ++ ["authentication", "password-based:built-in-database", "import_users"]),
-                        #{filename => CSVFileName}).
+    {ok, 204, _} = request(post, ImportUri, #{filename => CSVFileName}).
 
 %%------------------------------------------------------------------------------
 %% Helpers
@@ -440,10 +408,6 @@ delete_authenticators(Path, Chain) ->
                 Authenticators)
     end.
 
-response_data(Response) ->
-    #{<<"data">> := Data} = jiffy:decode(Response, [return_maps]),
-    Data.
-
 request(Method, Url) ->
     request(Method, Url, []).