From 7febcb852a8c41bdf0868c1b9a70bd7ddb83b5b6 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Tue, 27 Jul 2021 09:54:08 +0800 Subject: [PATCH] feat(authn http api): add test case and improve http api spec --- apps/emqx_authn/etc/emqx_authn.conf | 12 +- apps/emqx_authn/include/emqx_authn.hrl | 1 - apps/emqx_authn/src/emqx_authn.erl | 42 ++- apps/emqx_authn/src/emqx_authn_api.erl | 340 +++++++++--------- .../emqx_enhanced_authn_scram_mnesia.erl | 6 +- .../src/simple_authn/emqx_authn_mnesia.erl | 6 +- .../simple_authn/emqx_authn_other_schema.erl | 58 +++ apps/emqx_authn/test/emqx_authn_SUITE.erl | 60 ++-- apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl | 26 +- .../test/emqx_authn_mnesia_SUITE.erl | 94 +++-- 10 files changed, 361 insertions(+), 284 deletions(-) create mode 100644 apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl diff --git a/apps/emqx_authn/etc/emqx_authn.conf b/apps/emqx_authn/etc/emqx_authn.conf index 30ef427ae..7c1bc711c 100644 --- a/apps/emqx_authn/etc/emqx_authn.conf +++ b/apps/emqx_authn/etc/emqx_authn.conf @@ -1,11 +1,11 @@ emqx_authn: { enable: false authenticators: [ - { - name: "authenticator1" - mechanism: password-based - server_type: built-in-database - user_id_type: clientid - } + # { + # name: "authenticator1" + # mechanism: password-based + # server_type: built-in-database + # user_id_type: clientid + # } ] } diff --git a/apps/emqx_authn/include/emqx_authn.hrl b/apps/emqx_authn/include/emqx_authn.hrl index ef3e2893c..5a8bfb66a 100644 --- a/apps/emqx_authn/include/emqx_authn.hrl +++ b/apps/emqx_authn/include/emqx_authn.hrl @@ -26,7 +26,6 @@ , provider :: module() , config :: map() , state :: map() - , version :: binary() }). -record(chain, diff --git a/apps/emqx_authn/src/emqx_authn.erl b/apps/emqx_authn/src/emqx_authn.erl index 95ace3800..f41921e04 100644 --- a/apps/emqx_authn/src/emqx_authn.erl +++ b/apps/emqx_authn/src/emqx_authn.erl @@ -194,20 +194,25 @@ do_update_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, C false -> case CreateWhenNotFound of true -> - case do_create_authenticator(ChainID, AuthenticatorID, Config) of - {ok, Authenticator} -> - NAuthenticators = Authenticators ++ [{AuthenticatorID, NewName, Authenticator}], - ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), - {ok, serialize_authenticator(Authenticator)}; - {error, Reason} -> - {error, Reason} - end; + case lists:keymember(NewName, 2, Authenticators) of + true -> + {error, name_has_be_used}; + false -> + case do_create_authenticator(ChainID, AuthenticatorID, Config) of + {ok, Authenticator} -> + NAuthenticators = Authenticators ++ [{AuthenticatorID, NewName, Authenticator}], + ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NAuthenticators}, write), + {ok, serialize_authenticator(Authenticator)}; + {error, Reason} -> + {error, Reason} + end + end; false -> {error, {not_found, {authenticator, AuthenticatorID}}} end; {value, {_, _, #authenticator{provider = Provider, - state = #{version := Version} = State}}, + state = #{version := Version} = State} = Authenticator}, Others} -> case lists:keymember(NewName, 2, Others) of true -> @@ -215,12 +220,12 @@ do_update_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, C false -> case (NewProvider = authenticator_provider(Config)) =:= Provider of true -> - Unique = {ChainID, AuthenticatorID, Version}, + Unique = <>, case Provider:update(Config#{'_unique' => Unique}, State) of {ok, NewState} -> - NewAuthenticator = #authenticator{name = NewName, - config = Config, - state = switch_version(NewState)}, + NewAuthenticator = Authenticator#authenticator{name = NewName, + config = Config, + state = switch_version(NewState)}, NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}, write), {ok, serialize_authenticator(NewAuthenticator)}; @@ -228,12 +233,13 @@ do_update_authenticator(ChainID, AuthenticatorID, #{name := NewName} = Config, C {error, Reason} end; false -> - case NewProvider:create(Config#{'_unique' => {ChainID, AuthenticatorID, Version}}) of + Unique = <>, + case NewProvider:create(Config#{'_unique' => Unique}) of {ok, NewState} -> - NewAuthenticator = #authenticator{name = NewName, - provider = NewProvider, - config = Config, - state = switch_version(NewState)}, + NewAuthenticator = Authenticator#authenticator{name = NewName, + provider = NewProvider, + config = Config, + state = switch_version(NewState)}, NewAuthenticators = replace_authenticator(AuthenticatorID, NewAuthenticator, Authenticators), ok = mnesia:write(?CHAIN_TAB, Chain#chain{authenticators = NewAuthenticators}, write), _ = Provider:destroy(State), diff --git a/apps/emqx_authn/src/emqx_authn_api.erl b/apps/emqx_authn/src/emqx_authn_api.erl index c87df2dc5..1c4d0575d 100644 --- a/apps/emqx_authn/src/emqx_authn_api.erl +++ b/apps/emqx_authn/src/emqx_authn_api.erl @@ -29,6 +29,56 @@ , users2/2 ]). +-define(EXAMPLE_1, #{name => <<"example 1">>, + mechanism => <<"password-based">>, + server_type => <<"built-in-example">>, + user_id_type => <<"username">>, + password_hash_algorithm => #{ + name => <<"sha256">> + }}). + +-define(EXAMPLE_2, #{name => <<"example 2">>, + mechanism => <<"password-based">>, + server_type => <<"http-server">>, + method => <<"post">>, + url => <<"http://localhost:80/login">>, + headers => #{ + <<"content-type">> => <<"application/json">> + }, + form_data => #{ + <<"username">> => <<"${mqtt-username}">>, + <<"password">> => <<"${mqtt-password}">> + }}). + +-define(EXAMPLE_3, #{name => <<"example 3">>, + mechanism => <<"jwt">>, + use_jwks => false, + algorithm => <<"hmac-based">>, + secret => <<"mysecret">>, + secret_base64_encoded => false, + verify_claims => #{ + <<"username">> => <<"${mqtt-username}">> + }}). + +-define(ERR_RESPONSE(Desc), #{description => Desc, + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"error">>), + examples => #{ + example1 => #{ + summary => <<"Not Found">>, + value => #{code => <<"NOT_FOUND">>, message => <<"Authenticator '67e4c9d3' does not exist">>} + }, + example2 => #{ + summary => <<"Conflict">>, + value => #{code => <<"ALREADY_EXISTS">>, message => <<"Name has be used">>} + }, + example3 => #{ + summary => <<"Bad Request 1">>, + value => #{code => <<"OUT_OF_RANGE">>, message => <<"Out of range">>} + } + }}}}). + api_spec() -> {[ authenticators_api() , authenticators_api2() @@ -39,40 +89,6 @@ api_spec() -> ], definitions()}. authenticators_api() -> - Example1 = #{name => <<"example">>, - mechanism => <<"password-based">>, - config => #{ - server_type => <<"built-in-example">>, - user_id_type => <<"username">>, - password_hash_algorithm => #{ - name => <<"sha256">> - } - }}, - Example2 = #{name => <<"example">>, - mechanism => <<"password-based">>, - config => #{ - server_type => <<"http-server">>, - method => <<"post">>, - url => <<"http://localhost:80/login">>, - headers => #{ - <<"content-type">> => <<"application/json">> - }, - form_data => #{ - <<"username">> => <<"${mqtt-username}">>, - <<"password">> => <<"${mqtt-password}">> - } - }}, - Example3 = #{name => <<"example">>, - mechanism => <<"jwt">>, - config => #{ - use_jwks => false, - algorithm => <<"hmac-based">>, - secret => <<"mysecret">>, - secret_base64_encoded => false, - verify_claims => #{ - <<"username">> => <<"${mqtt-username}">> - } - }}, Metadata = #{ post => #{ description => "Create authenticator", @@ -83,15 +99,15 @@ authenticators_api() -> examples => #{ default => #{ summary => <<"Default">>, - value => emqx_json:encode(Example1) + value => emqx_json:encode(?EXAMPLE_1) }, http => #{ summary => <<"Authentication provided by HTTP Server">>, - value => emqx_json:encode(Example2) + value => emqx_json:encode(?EXAMPLE_2) }, jwt => #{ summary => <<"JWT Authentication">>, - value => emqx_json:encode(Example3) + value => emqx_json:encode(?EXAMPLE_3) } } } @@ -102,10 +118,26 @@ authenticators_api() -> description => <<"Created">>, content => #{ 'application/json' => #{ - schema => minirest:ref(<<"returned_authenticator">>) + schema => minirest:ref(<<"returned_authenticator">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) + } + } } } - } + }, + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) } }, get => #{ @@ -118,6 +150,15 @@ authenticators_api() -> schema => #{ type => array, items => minirest:ref(<<"returned_authenticator">>) + }, + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode([ maps:put(id, <<"example 1">>, ?EXAMPLE_1) + , maps:put(id, <<"example 2">>, ?EXAMPLE_2) + , maps:put(id, <<"example 3">>, ?EXAMPLE_3) + ]) + } } } } @@ -146,18 +187,25 @@ authenticators_api2() -> description => <<"OK">>, content => #{ 'application/json' => #{ - schema => minirest:ref(<<"returned_authenticator">>) + schema => minirest:ref(<<"returned_authenticator">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) + } + } } } }, - <<"404">> => #{ - description => <<"Not Found">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>) - } - } - } + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } }, put => #{ @@ -180,6 +228,16 @@ authenticators_api2() -> , minirest:ref(<<"jwt">>) , minirest:ref(<<"scram">>) ] + }, + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(?EXAMPLE_1) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(?EXAMPLE_2) + } } } } @@ -189,18 +247,27 @@ authenticators_api2() -> description => <<"OK">>, content => #{ 'application/json' => #{ - schema => minirest:ref(<<"returned_authenticator">>) + schema => minirest:ref(<<"returned_authenticator">>), + examples => #{ + example1 => #{ + summary => <<"Example 1">>, + value => emqx_json:encode(maps:put(id, <<"example 1">>, ?EXAMPLE_1)) + }, + example2 => #{ + summary => <<"Example 2">>, + value => emqx_json:encode(maps:put(id, <<"example 2">>, ?EXAMPLE_2)) + }, + example3 => #{ + summary => <<"Example 3">>, + value => emqx_json:encode(maps:put(id, <<"example 3">>, ?EXAMPLE_3)) + } + } } } }, - <<"404">> => #{ - description => <<"Not Found">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>) - } - } - } + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>), + <<"409">> => ?ERR_RESPONSE(<<"Conflict">>) } }, delete => #{ @@ -219,14 +286,7 @@ authenticators_api2() -> <<"204">> => #{ description => <<"No Content">> }, - <<"404">> => #{ - description => <<"Not Found">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>) - } - } - } + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } } }, @@ -266,14 +326,8 @@ position_api() -> <<"204">> => #{ description => <<"No Content">> }, - <<"404">> => #{ - description => <<"Not Found">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>) - } - } - } + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } } }, @@ -312,22 +366,8 @@ import_users_api() -> <<"204">> => #{ description => <<"No Content">> }, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>) - } - } - }, - <<"404">> => #{ - description => <<"Not Found">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>) - } - } - } + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } } }, @@ -382,14 +422,8 @@ users_api() -> } } }, - <<"400">> => #{ - description => <<"Bad Request">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>) - } - } - } + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } }, get => #{ @@ -423,7 +457,8 @@ users_api() -> } } } - } + }, + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } } }, @@ -486,14 +521,8 @@ users2_api() -> } } }, - <<"404">> => #{ - description => <<"Not Found">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>) - } - } - } + <<"400">> => ?ERR_RESPONSE(<<"Bad Request">>), + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } }, get => #{ @@ -536,14 +565,7 @@ users2_api() -> } } }, - <<"404">> => #{ - description => <<"Not Found">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>) - } - } - } + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } }, delete => #{ @@ -570,14 +592,7 @@ users2_api() -> <<"204">> => #{ description => <<"No Content">> }, - <<"404">> => #{ - description => <<"Not Found">>, - content => #{ - 'application/json' => #{ - schema => minirest:ref(<<"error">>) - } - } - } + <<"404">> => ?ERR_RESPONSE(<<"Not Found">>) } } }, @@ -1007,47 +1022,43 @@ authenticators2(delete, Request) -> position(post, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - case emqx_json:decode(Body, [return_maps]) of - #{<<"position">> := Position} when is_integer(Position) -> - case emqx_authn:move_authenticator_to_the_nth(?CHAIN, AuthenticatorID, Position) of - ok -> - {204}; - {error, Reason} -> - serialize_error(Reason) - end; - _ -> - serialize_error({missing_parameter, position}) + NBody = emqx_json:decode(Body, [return_maps]), + Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"position">> => NBody}, + #{nullable => true}, ["position"]), + #{position := #{position := Position}} = emqx_map_lib:unsafe_atom_key_map(Config), + case emqx_authn:move_authenticator_to_the_nth(?CHAIN, AuthenticatorID, Position) of + ok -> + {204}; + {error, Reason} -> + serialize_error(Reason) end. import_users(post, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - case emqx_json:decode(Body, [return_maps]) of - #{<<"filename">> := Filename} when is_binary(Filename) -> - case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of - ok -> - {204}; - {error, Reason} -> - serialize_error(Reason) - end; - _ -> - serialize_error({missing_parameter, filename}) + NBody = emqx_json:decode(Body, [return_maps]), + Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"filename">> => NBody}, + #{nullable => true}, ["filename"]), + #{filename := #{filename := Filename}} = emqx_map_lib:unsafe_atom_key_map(Config), + case emqx_authn:import_users(?CHAIN, AuthenticatorID, Filename) of + ok -> + {204}; + {error, Reason} -> + serialize_error(Reason) end. users(post, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - case emqx_json:decode(Body, [return_maps]) of - #{<<"user_id">> := _, - <<"password">> := _} = UserInfo -> - case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserInfo) of - {ok, User} -> - {201, User}; - {error, Reason} -> - serialize_error(Reason) - end; - _ -> - serialize_error({missing_parameter, user_id}) + NBody = emqx_json:decode(Body, [return_maps]), + Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"user_info">> => NBody}, + #{nullable => true}, ["user_info"]), + #{user_info := UserInfo} = emqx_map_lib:unsafe_atom_key_map(Config), + case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserInfo) of + {ok, User} -> + {201, User}; + {error, Reason} -> + serialize_error(Reason) end; users(get, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), @@ -1062,16 +1073,15 @@ users2(patch, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), UserID = cowboy_req:binding(user_id, Request), {ok, Body, _} = cowboy_req:read_body(Request), - case emqx_json:decode(Body, [return_maps]) of - #{<<"password">> := _} = UserInfo -> - case emqx_authn:add_user(?CHAIN, AuthenticatorID, UserID, UserInfo) of - {ok, User} -> - {200, User}; - {error, Reason} -> - serialize_error(Reason) - end; - _ -> - serialize_error({missing_parameter, password}) + NBody = emqx_json:decode(Body, [return_maps]), + Config = hocon_schema:check_plain(emqx_authn_other_schema, #{<<"new_user_info">> => NBody}, + #{nullable => true}, ["new_user_info"]), + #{new_user_info := NewUserInfo} = emqx_map_lib:unsafe_atom_key_map(Config), + case emqx_authn:update_user(?CHAIN, AuthenticatorID, UserID, NewUserInfo) of + {ok, User} -> + {200, User}; + {error, Reason} -> + serialize_error(Reason) end; users2(get, Request) -> AuthenticatorID = cowboy_req:binding(id, Request), @@ -1106,6 +1116,6 @@ serialize_error({missing_parameter, Name}) -> message => list_to_binary( io_lib:format("The input parameter '~p' that is mandatory for processing this request is not supplied", [Name]) )}}; -serialize_error(_) -> +serialize_error(Reason) -> {400, #{code => <<"BAD_REQUEST">>, - message => <<"Todo">>}}. \ No newline at end of file + message => list_to_binary(io_lib:format("Todo: ~p", [Reason]))}}. \ No newline at end of file diff --git a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl index 9b147dceb..52c301946 100644 --- a/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl +++ b/apps/emqx_authn/src/enhanced_authn/emqx_enhanced_authn_scram_mnesia.erl @@ -133,8 +133,8 @@ destroy(#{user_group := UserGroup}) -> end). %% TODO: binary to atom -add_user(#{<<"user_id">> := UserID, - <<"password">> := Password}, #{user_group := UserGroup} = State) -> +add_user(#{user_id := UserID, + password := Password}, #{user_group := UserGroup} = State) -> trans( fun() -> case mnesia:read(?TAB, {UserGroup, UserID}, write) of @@ -157,7 +157,7 @@ delete_user(UserID, #{user_group := UserGroup}) -> end end). -update_user(UserID, #{<<"password">> := Password}, +update_user(UserID, #{password := Password}, #{user_group := UserGroup} = State) -> trans( fun() -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl index 3d5ebe7a0..ce845d4e3 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_mnesia.erl @@ -178,8 +178,8 @@ import_users(Filename0, State) -> {error, {unsupported_file_format, Extension}} end. -add_user(#{<<"user_id">> := UserID, - <<"password">> := Password}, +add_user(#{user_id := UserID, + password := Password}, #{user_group := UserGroup} = State) -> trans( fun() -> @@ -203,7 +203,7 @@ delete_user(UserID, #{user_group := UserGroup}) -> end end). -update_user(UserID, #{<<"password">> := Password}, +update_user(UserID, #{password := Password}, #{user_group := UserGroup} = State) -> trans( fun() -> diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl new file mode 100644 index 000000000..0f5c8abb8 --- /dev/null +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_other_schema.erl @@ -0,0 +1,58 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%%-------------------------------------------------------------------- + +-module(emqx_authn_other_schema). + +-include("emqx_authn.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-behaviour(hocon_schema). + +-export([ structs/0 + , fields/1 + ]). + +structs() -> [ "filename", "position", "user_info", "new_user_info"]. + +fields("filename") -> + [ {filename, fun filename/1} ]; +fields("position") -> + [ {position, fun position/1} ]; +fields("user_info") -> + [ {user_id, fun user_id/1} + , {password, fun password/1} + ]; +fields("new_user_info") -> + [ {password, fun password/1} + ]. + +filename(type) -> string(); +filename(nullable) -> false; +filename(_) -> undefined. + +position(type) -> integer(); +position(validate) -> [fun (Position) -> Position > 0 end]; +position(nullable) -> false; +position(_) -> undefined. + +user_id(type) -> binary(); +user_id(nullable) -> false; +user_id(_) -> undefined. + +password(type) -> binary(); +password(nullable) -> false; +password(_) -> undefined. + diff --git a/apps/emqx_authn/test/emqx_authn_SUITE.erl b/apps/emqx_authn/test/emqx_authn_SUITE.erl index 59103662c..36b0eabf3 100644 --- a/apps/emqx_authn/test/emqx_authn_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_SUITE.erl @@ -54,34 +54,44 @@ t_authenticator(_) -> AuthenticatorName1 = <<"myauthenticator1">>, AuthenticatorConfig1 = #{name => AuthenticatorName1, mechanism => 'password-based', - config => #{ - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}}, - ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), - ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName1)), - ?assertEqual({ok, [AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual({error, {already_exists, {authenticator, AuthenticatorName1}}}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), + server_type => 'built-in-database', + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}, + {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), + ?assertMatch({ok, #{name := AuthenticatorName1}}, ?AUTH:lookup_authenticator(?CHAIN, ID1)), + ?assertMatch({ok, [#{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), + ?assertEqual({error, name_has_be_used}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), + + AuthenticatorConfig2 = #{name => AuthenticatorName1, + mechanism => jwt, + use_jwks => false, + algorithm => 'hmac-based', + secret => <<"abcdef">>, + secret_base64_encoded => false, + verify_claims => []}, + {ok, #{name := AuthenticatorName1, id := ID1, mechanism := jwt}} = ?AUTH:update_authenticator(?CHAIN, ID1, AuthenticatorConfig2), + + ID2 = <<"random">>, + ?assertEqual({error, {not_found, {authenticator, ID2}}}, ?AUTH:update_authenticator(?CHAIN, ID2, AuthenticatorConfig2)), + ?assertEqual({error, name_has_be_used}, ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig2)), AuthenticatorName2 = <<"myauthenticator2">>, - AuthenticatorConfig2 = AuthenticatorConfig1#{name => AuthenticatorName2}, - ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)), - ?assertMatch({ok, #{id := ?CHAIN, authenticators := [AuthenticatorConfig1, AuthenticatorConfig2]}}, ?AUTH:lookup_chain(?CHAIN)), - ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:lookup_authenticator(?CHAIN, AuthenticatorName2)), - ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)), + AuthenticatorConfig3 = AuthenticatorConfig2#{name => AuthenticatorName2}, + {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"abcdef">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3), + ?assertMatch({ok, #{name := AuthenticatorName2}}, ?AUTH:lookup_authenticator(?CHAIN, ID2)), + {ok, #{name := AuthenticatorName2, id := ID2, secret := <<"fedcba">>}} = ?AUTH:update_or_create_authenticator(?CHAIN, ID2, AuthenticatorConfig3#{secret := <<"fedcba">>}), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)), - ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_end(?CHAIN, AuthenticatorName2)), - ?assertEqual({ok, [AuthenticatorConfig1, AuthenticatorConfig2]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 1)), - ?assertEqual({ok, [AuthenticatorConfig2, AuthenticatorConfig1]}, ?AUTH:list_authenticators(?CHAIN)), - ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 3)), - ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, AuthenticatorName2, 0)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)), + ?assertMatch({ok, #{id := ?CHAIN, authenticators := [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}}, ?AUTH:lookup_chain(?CHAIN)), + ?assertMatch({ok, [#{name := AuthenticatorName1}, #{name := AuthenticatorName2}]}, ?AUTH:list_authenticators(?CHAIN)), + + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)), + ?assertMatch({ok, [#{name := AuthenticatorName2}, #{name := AuthenticatorName1}]}, ?AUTH:list_authenticators(?CHAIN)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 3)), + ?assertEqual({error, out_of_range}, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 0)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), ?assertEqual({ok, []}, ?AUTH:list_authenticators(?CHAIN)), ok. diff --git a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl index b22e72a95..7435deaa0 100644 --- a/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_jwt_SUITE.erl @@ -39,15 +39,14 @@ end_per_suite(_) -> t_jwt_authenticator(_) -> AuthenticatorName = <<"myauthenticator">>, - Config = #{use_jwks => false, + Config = #{name => AuthenticatorName, + mechanism => jwt, + use_jwks => false, algorithm => 'hmac-based', secret => <<"abcdef">>, secret_base64_encoded => false, verify_claims => []}, - AuthenticatorConfig = #{name => AuthenticatorName, - mechanism => jwt, - config => Config}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), + {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), Payload = #{<<"username">> => <<"myuser">>}, JWS = generate_jws('hmac-based', Payload, <<"abcdef">>), @@ -62,11 +61,11 @@ t_jwt_authenticator(_) -> %% secret_base64_encoded Config2 = Config#{secret => base64:encode(<<"abcdef">>), secret_base64_encoded => true}, - ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, AuthenticatorName, Config2)), + ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config2)), ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), Config3 = Config#{verify_claims => [{<<"username">>, <<"${mqtt-username}">>}]}, - ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, AuthenticatorName, Config3)), + ?assertMatch({ok, _}, ?AUTH:update_authenticator(?CHAIN, ID, Config3)), ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo#{username => <<"otheruser">>}, ok)), @@ -109,7 +108,7 @@ t_jwt_authenticator(_) -> ClientInfo8 = ClientInfo#{password => JWS8}, ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo8, ok)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ok. t_jwt_authenticator2(_) -> @@ -117,14 +116,13 @@ t_jwt_authenticator2(_) -> PublicKey = list_to_binary(filename:join([Dir, "data/public_key.pem"])), PrivateKey = list_to_binary(filename:join([Dir, "data/private_key.pem"])), AuthenticatorName = <<"myauthenticator">>, - Config = #{use_jwks => false, + Config = #{name => AuthenticatorName, + mechanism => jwt, + use_jwks => false, algorithm => 'public-key', certificate => PublicKey, verify_claims => []}, - AuthenticatorConfig = #{name => AuthenticatorName, - mechanism => jwt, - config => Config}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), + {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, Config), Payload = #{<<"username">> => <<"myuser">>}, JWS = generate_jws('public-key', Payload, PrivateKey), @@ -133,7 +131,7 @@ t_jwt_authenticator2(_) -> ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo, ok)), ?assertEqual({stop, {error, not_authorized}}, ?AUTH:authenticate(ClientInfo#{password => <<"badpassword">>}, ok)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ok. generate_jws('hmac-based', Payload, Secret) -> diff --git a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl index c8c787cf1..511d00e79 100644 --- a/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl +++ b/apps/emqx_authn/test/emqx_authn_mnesia_SUITE.erl @@ -41,18 +41,17 @@ t_mnesia_authenticator(_) -> AuthenticatorName = <<"myauthenticator">>, AuthenticatorConfig = #{name => AuthenticatorName, mechanism => 'password-based', - config => #{ - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), + server_type => 'built-in-database', + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}, + {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), UserInfo = #{<<"user_id">> => <<"myuser">>, <<"password">> => <<"mypass">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), ClientInfo = #{zone => external, username => <<"myuser">>, @@ -70,39 +69,38 @@ t_mnesia_authenticator(_) -> ?assertEqual({error, bad_username_or_password}, emqx_access_control:authenticate(ClientInfo3)), UserInfo2 = UserInfo#{<<"password">> => <<"mypass2">>}, - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, AuthenticatorName, <<"myuser">>, UserInfo2)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:update_user(?CHAIN, ID, <<"myuser">>, UserInfo2)), ClientInfo4 = ClientInfo#{password => <<"mypass2">>}, ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo4, ok)), - ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, AuthenticatorName, <<"myuser">>)), - ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_user(?CHAIN, ID, <<"myuser">>)), + ?assertEqual({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), - ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, AuthenticatorName, UserInfo)), - ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), + ?assertEqual({ok, #{user_id => <<"myuser">>}}, ?AUTH:add_user(?CHAIN, ID, UserInfo)), + ?assertMatch({ok, #{user_id := <<"myuser">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), - ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser">>)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), + {ok, #{name := AuthenticatorName, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), + ?assertMatch({error, not_found}, ?AUTH:lookup_user(?CHAIN, ID1, <<"myuser">>)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), ok. t_import(_) -> AuthenticatorName = <<"myauthenticator">>, AuthenticatorConfig = #{name => AuthenticatorName, mechanism => 'password-based', - config => #{ - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}}, - ?assertEqual({ok, AuthenticatorConfig}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig)), + server_type => 'built-in-database', + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}, + {ok, #{name := AuthenticatorName, id := ID}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig), Dir = code:lib_dir(emqx_authn, test), - ?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.json"]))), - ?assertEqual(ok, ?AUTH:import_users(?CHAIN, AuthenticatorName, filename:join([Dir, "data/user-credentials.csv"]))), - ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser1">>)), - ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, AuthenticatorName, <<"myuser3">>)), + ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.json"]))), + ?assertEqual(ok, ?AUTH:import_users(?CHAIN, ID, filename:join([Dir, "data/user-credentials.csv"]))), + ?assertMatch({ok, #{user_id := <<"myuser1">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser1">>)), + ?assertMatch({ok, #{user_id := <<"myuser3">>}}, ?AUTH:lookup_user(?CHAIN, ID, <<"myuser3">>)), ClientInfo1 = #{username => <<"myuser1">>, password => <<"mypassword1">>}, @@ -110,37 +108,35 @@ t_import(_) -> ClientInfo2 = ClientInfo1#{username => <<"myuser3">>, password => <<"mypassword3">>}, ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID)), ok. t_multi_mnesia_authenticator(_) -> AuthenticatorName1 = <<"myauthenticator1">>, AuthenticatorConfig1 = #{name => AuthenticatorName1, mechanism => 'password-based', - config => #{ - server_type => 'built-in-database', - user_id_type => username, - password_hash_algorithm => #{ - name => sha256 - }}}, + server_type => 'built-in-database', + user_id_type => username, + password_hash_algorithm => #{ + name => sha256 + }}, AuthenticatorName2 = <<"myauthenticator2">>, AuthenticatorConfig2 = #{name => AuthenticatorName2, mechanism => 'password-based', - config => #{ - server_type => 'built-in-database', - user_id_type => clientid, - password_hash_algorithm => #{ - name => sha256 - }}}, - ?assertEqual({ok, AuthenticatorConfig1}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1)), - ?assertEqual({ok, AuthenticatorConfig2}, ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2)), + server_type => 'built-in-database', + user_id_type => clientid, + password_hash_algorithm => #{ + name => sha256 + }}, + {ok, #{name := AuthenticatorName1, id := ID1}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig1), + {ok, #{name := AuthenticatorName2, id := ID2}} = ?AUTH:create_authenticator(?CHAIN, AuthenticatorConfig2), ?assertEqual({ok, #{user_id => <<"myuser">>}}, - ?AUTH:add_user(?CHAIN, AuthenticatorName1, + ?AUTH:add_user(?CHAIN, ID1, #{<<"user_id">> => <<"myuser">>, <<"password">> => <<"mypass1">>})), ?assertEqual({ok, #{user_id => <<"myclient">>}}, - ?AUTH:add_user(?CHAIN, AuthenticatorName2, + ?AUTH:add_user(?CHAIN, ID2, #{<<"user_id">> => <<"myclient">>, <<"password">> => <<"mypass2">>})), @@ -148,12 +144,12 @@ t_multi_mnesia_authenticator(_) -> clientid => <<"myclient">>, password => <<"mypass1">>}, ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo1, ok)), - ?assertEqual(ok, ?AUTH:move_authenticator_to_the_front(?CHAIN, AuthenticatorName2)), + ?assertEqual(ok, ?AUTH:move_authenticator_to_the_nth(?CHAIN, ID2, 1)), ?assertEqual({stop, {error, bad_username_or_password}}, ?AUTH:authenticate(ClientInfo1, ok)), ClientInfo2 = ClientInfo1#{password => <<"mypass2">>}, ?assertEqual({stop, ok}, ?AUTH:authenticate(ClientInfo2, ok)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName1)), - ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, AuthenticatorName2)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID1)), + ?assertEqual(ok, ?AUTH:delete_authenticator(?CHAIN, ID2)), ok.