From 8252771306357ecf9b65004998c9e47ac8ae410a Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Wed, 1 Sep 2021 15:56:15 +0800 Subject: [PATCH] feat(authz api): support upload ssl cert file for api --- apps/emqx_authz/src/emqx_authz.erl | 2 +- apps/emqx_authz/src/emqx_authz_api_schema.erl | 26 ++- .../emqx_authz/src/emqx_authz_api_sources.erl | 163 +++++++++--------- .../test/emqx_authz_api_sources_SUITE.erl | 66 +++++-- 4 files changed, 157 insertions(+), 100 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 4a6d7033e..f3b9a4793 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -309,7 +309,7 @@ check_sources(RawSources) -> find_source_by_type(Type) -> find_source_by_type(Type, lookup()). find_source_by_type(Type, Sources) -> find_source_by_type(Type, Sources, 1). -find_source_by_type(_, [], _N) -> error(not_found_rule); +find_source_by_type(_, [], _N) -> error(not_found_source); find_source_by_type(Type, [ Source = #{type := T} | Tail], N) -> case Type =:= T of true -> {N, Source}; diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 4c17cd0b6..7d8b583ab 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -26,6 +26,9 @@ definitions() -> type => object, required => [status], properties => #{ + id => #{ + type => string + }, status => #{ type => string, example => <<"healthy">> @@ -41,7 +44,18 @@ definitions() -> oneOf => [ minirest:ref(<<"connector_redis">>) ] }, - ConnectorRedis= #{ + SSL = #{ + type => object, + required => [enable], + properties => #{ + enable => #{type => boolean, example => true}, + cacertfile => #{type => string}, + keyfile => #{type => string}, + certfile => #{type => string}, + verify => #{type => boolean, example => false} + } + }, + ConnectorRedis = #{ type => object, required => [type, enable, config, cmd], properties => #{ @@ -65,7 +79,8 @@ definitions() -> pool_size => #{type => integer}, auto_reconnect => #{type => boolean, example => true}, password => #{type => string}, - database => #{type => string, example => mqtt} + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) } } , #{type => object, @@ -80,7 +95,8 @@ definitions() -> pool_size => #{type => integer}, auto_reconnect => #{type => boolean, example => true}, password => #{type => string}, - database => #{type => string, example => mqtt} + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) } } , #{type => object, @@ -94,7 +110,8 @@ definitions() -> pool_size => #{type => integer}, auto_reconnect => #{type => boolean, example => true}, password => #{type => string}, - database => #{type => string, example => mqtt} + database => #{type => integer}, + ssl => minirest:ref(<<"ssl">>) } } ], @@ -108,5 +125,6 @@ definitions() -> }, [ #{<<"returned_sources">> => RetruenedSources} , #{<<"sources">> => Sources} + , #{<<"ssl">> => SSL} , #{<<"connector_redis">> => ConnectorRedis} ]. diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index 2ad5db1da..a735d5d70 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -19,6 +19,7 @@ -behavior(minirest_api). -include("emqx_authz.hrl"). +-include_lib("emqx/include/logger.hrl"). -define(EXAMPLE_REDIS, #{type=> redis, @@ -32,7 +33,7 @@ maps:put(annotations, #{status => healthy}, ?EXAMPLE_REDIS) ). --define(EXAMPLE_RETURNED_RULES, +-define(EXAMPLE_RETURNED, #{sources => [?EXAMPLE_RETURNED_REDIS ] }). @@ -55,24 +56,6 @@ sources_api() -> Metadata = #{ get => #{ description => "List authorization sources", - parameters => [ - #{ - name => page, - in => query, - schema => #{ - type => integer - }, - required => false - }, - #{ - name => limit, - in => query, - schema => #{ - type => integer - }, - required => false - } - ], responses => #{ <<"200">> => #{ description => <<"OK">>, @@ -90,7 +73,7 @@ sources_api() -> examples => #{ sources => #{ summary => <<"Sources">>, - value => jsx:encode(?EXAMPLE_RETURNED_RULES) + value => jsx:encode(?EXAMPLE_RETURNED) } } } @@ -287,53 +270,38 @@ move_source_api() -> }, {"/authorization/sources/:type/move", Metadata, move_source}. -sources(get, #{query_string := Query}) -> - Sources = lists:foldl(fun (#{type := _Type, enable := true, config := #{server := Server} = Config, annotations := #{id := Id}} = Source, AccIn) -> - NSource = case emqx_resource:health_check(Id) of - ok -> - Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => healthy}}; - _ -> - Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{id => Id, - status => unhealthy}} - end, - lists:append(AccIn, [NSource]); - (#{type := _Type, enable := true, annotations := #{id := Id}} = Source, AccIn) -> - NSource = case emqx_resource:health_check(Id) of - ok -> - Source#{annotations => #{status => healthy}}; - _ -> - Source#{annotations => #{status => unhealthy}} - end, - lists:append(AccIn, [NSource]); - (Source, AccIn) -> - lists:append(AccIn, [Source]) +sources(get, _) -> + Sources = lists:foldl(fun (#{type := _Type, enable := true, config := Config, annotations := #{id := Id}} = Source, AccIn) -> + NSource0 = case maps:get(server, Config, undefined) of + undefined -> Source; + Server -> + Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}} + end, + NSource1 = case maps:get(servers, Config, undefined) of + undefined -> NSource0; + Servers -> + NSource0#{config => Config#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}} + end, + NSource2 = case emqx_resource:health_check(Id) of + ok -> + NSource1#{annotations => #{status => healthy}}; + _ -> + NSource1#{annotations => #{status => unhealthy}} + end, + lists:append(AccIn, [NSource2]); + (Source, AccIn) -> + lists:append(AccIn, [Source#{annotations => #{status => healthy}}]) end, [], emqx_authz:lookup()), - case maps:is_key(<<"page">>, Query) andalso maps:is_key(<<"limit">>, Query) of - true -> - Page = maps:get(<<"page">>, Query), - Limit = maps:get(<<"limit">>, Query), - Index = (binary_to_integer(Page) - 1) * binary_to_integer(Limit), - {_, Sources1} = lists:split(Index, Sources), - case binary_to_integer(Limit) < length(Sources1) of - true -> - {Sources2, _} = lists:split(binary_to_integer(Limit), Sources1), - {200, #{sources => Sources2}}; - false -> {200, #{sources => Sources1}} - end; - false -> {200, #{sources => Sources}} - end; -sources(post, #{body := RawConfig}) -> - case emqx_authz:update(head, [RawConfig]) of + {200, #{sources => Sources}}; +sources(post, #{body := Body}) -> + case emqx_authz:update(head, [save_cert(Body)]) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end; -sources(put, #{body := RawConfig}) -> - case emqx_authz:update(replace, RawConfig) of +sources(put, #{body := Body}) -> + case emqx_authz:update(replace, save_cert(Body)) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, @@ -345,27 +313,28 @@ source(get, #{bindings := #{type := Type}}) -> {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; #{enable := false} = Source -> {200, Source}; #{type := file} = Source -> {200, Source}; - #{config := #{server := Server, - annotations := #{id := Id} - } = Config} = Source -> - case emqx_resource:health_check(Id) of + #{config := Config, annotations := #{id := Id}} = Source -> + NSource0 = case maps:get(server, Config, undefined) of + undefined -> Source; + Server -> + Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}} + end, + NSource1 = case maps:get(servers, Config, undefined) of + undefined -> NSource0; + Servers -> + NSource0#{config => Config#{servers => [emqx_connector_schema_lib:ip_port_to_string(Server) || Server <- Servers]}} + end, + NSource2 = case emqx_resource:health_check(Id) of ok -> - {200, Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{status => healthy}}}; + NSource1#{annotations => #{status => healthy}}; _ -> - {200, Source#{config => Config#{server => emqx_connector_schema_lib:ip_port_to_string(Server)}, - annotations => #{status => unhealthy}}} - end; - #{config := #{annotations := #{id := Id}}} = Source -> - case emqx_resource:health_check(Id) of - ok -> - {200, Source#{annotations => #{status => healthy}}}; - _ -> - {200, Source#{annotations => #{status => unhealthy}}} - end + NSource1#{annotations => #{status => unhealthy}} + end, + {200, NSource2} end; -source(put, #{bindings := #{type := Type}, body := RawConfig}) -> - case emqx_authz:update({replace_once, Type}, RawConfig) of +source(put, #{bindings := #{type := Type}, body := Body}) -> + + case emqx_authz:update({replace_once, Type}, save_cert(Body)) of {ok, _} -> {204}; {error, not_found_source} -> {404, #{code => <<"NOT_FOUND">>, @@ -391,3 +360,39 @@ move_source(post, #{bindings := #{type := Type}, body := #{<<"position">> := Pos {400, #{code => <<"BAD_REQUEST">>, messgae => atom_to_binary(Reason)}} end. + +save_cert(#{<<"config">> := #{<<"ssl">> := #{<<"enable">> := true} = SSL} = Config} = Body) -> + CertPath = filename:join([emqx:get_config([node, data_dir]), "certs"]), + CaCert = case maps:is_key(<<"cacertfile">>, SSL) of + true -> + write_file(filename:join([CertPath, "cacert-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"cacertfile">>, SSL)); + false -> "" + end, + Cert = case maps:is_key(<<"certfile">>, SSL) of + true -> + write_file(filename:join([CertPath, "cert-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"certfile">>, SSL)); + false -> "" + end, + Key = case maps:is_key(<<"keyfile">>, SSL) of + true -> + write_file(filename:join([CertPath, "key-" ++ emqx_rule_id:gen() ++".pem"]), + maps:get(<<"keyfile">>, SSL)); + false -> "" + end, + Body#{<<"config">> := Config#{<<"ssl">> => SSL#{<<"cacertfile">> => CaCert, + <<"certfile">> => Cert, + <<"keyfile">> => Key} + } + }; +save_cert(Body) -> Body. + +write_file(Filename, Bytes) -> + ok = filelib:ensure_dir(Filename), + case file:write_file(Filename, Bytes) of + ok -> Filename; + {error, Reason} -> + ?LOG(error, "Write File ~p Error: ~p", [Filename, Reason]), + error(Reason) + end. diff --git a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl index 55185de78..ed3cf18d0 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -48,8 +48,10 @@ -define(SOURCE2, #{<<"type">> => <<"mongo">>, <<"enable">> => true, <<"config">> => #{ - <<"mongo_type">> => <<"single">>, - <<"server">> => <<"127.0.0.1:27017">>, + <<"mongo_type">> => <<"sharded">>, + <<"servers">> => [<<"127.0.0.1:27017">>, + <<"192.168.0.1:27017">> + ], <<"pool_size">> => 1, <<"database">> => <<"mqtt">>, <<"ssl">> => #{<<"enable">> => false}}, @@ -59,7 +61,7 @@ -define(SOURCE3, #{<<"type">> => <<"mysql">>, <<"enable">> => true, <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, + <<"server">> => <<"127.0.0.1:3306">>, <<"pool_size">> => 1, <<"database">> => <<"mqtt">>, <<"username">> => <<"xx">>, @@ -71,7 +73,7 @@ -define(SOURCE4, #{<<"type">> => <<"pgsql">>, <<"enable">> => true, <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, + <<"server">> => <<"127.0.0.1:5432">>, <<"pool_size">> => 1, <<"database">> => <<"mqtt">>, <<"username">> => <<"xx">>, @@ -83,12 +85,15 @@ -define(SOURCE5, #{<<"type">> => <<"redis">>, <<"enable">> => true, <<"config">> => #{ - <<"server">> => <<"127.0.0.1:27017">>, + <<"servers">> => [<<"127.0.0.1:6379">>, + <<"127.0.0.1:6380">> + ], <<"pool_size">> => 1, <<"database">> => 0, <<"password">> => <<"ee">>, <<"auto_reconnect">> => true, - <<"ssl">> => #{<<"enable">> => false}}, + <<"ssl">> => #{<<"enable">> => false} + }, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). @@ -144,6 +149,26 @@ set_special_configs(emqx_authz) -> set_special_configs(_App) -> ok. +init_per_testcase(t_api, Config) -> + meck:new(emqx_rule_id, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx_rule_id, gen, fun() -> "fake" end), + + meck:new(emqx, [non_strict, passthrough, no_history, no_link]), + meck:expect(emqx, get_config, fun([node, data_dir]) -> + % emqx_ct_helpers:deps_path(emqx_authz, "test"); + {data_dir, Data} = lists:keyfind(data_dir, 1, Config), + Data; + (C) -> meck:passthrough([C]) + end), + Config; +init_per_testcase(_, Config) -> Config. + +end_per_testcase(t_api, _Config) -> + meck:unload(emqx_rule_id), + meck:unload(emqx), + ok; +end_per_testcase(_, _Config) -> ok. + %%------------------------------------------------------------------------------ %% Testcases %%------------------------------------------------------------------------------ @@ -158,13 +183,6 @@ t_api(_) -> {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []), ?assertEqual(20, length(get_sources(Result2))), - lists:foreach(fun(Page) -> - Query = "?page=" ++ integer_to_list(Page) ++ "&&limit=10", - Url = uri(["authorization/sources" ++ Query]), - {ok, 200, Result} = request(get, Url, []), - ?assertEqual(10, length(get_sources(Result))) - end, lists:seq(1, 2)), - {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), {ok, 200, Result3} = request(get, uri(["authorization", "sources"]), []), @@ -176,15 +194,31 @@ t_api(_) -> ], Sources), {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), - {ok, 200, Result4} = request(get, uri(["authorization", "sources", "http"]), []), ?assertMatch(#{<<"type">> := <<"http">>, <<"enable">> := false}, jsx:decode(Result4)), + #{<<"config">> := Config} = ?SOURCE2, + {ok, 204, _} = request(put, uri(["authorization", "sources", "mongo"]), + ?SOURCE2#{<<"config">> := Config#{<<"ssl">> := #{ + <<"enable">> => true, + <<"cacertfile">> => <<"fake cacert file">>, + <<"certfile">> => <<"fake cert file">>, + <<"keyfile">> => <<"fake key file">>, + <<"verify">> => false + }}}), + {ok, 200, Result5} = request(get, uri(["authorization", "sources", "mongo"]), []), + ?assertMatch(#{<<"type">> := <<"mongo">>, + <<"config">> := #{<<"ssl">> := #{<<"enable">> := true}} + }, jsx:decode(Result5)), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cacert-fake.pem"]))), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "cert-fake.pem"]))), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "certs", "key-fake.pem"]))), + lists:foreach(fun(#{<<"type">> := Type}) -> {ok, 204, _} = request(delete, uri(["authorization", "sources", binary_to_list(Type)]), []) end, Sources), - {ok, 200, Result5} = request(get, uri(["authorization", "sources"]), []), - ?assertEqual([], get_sources(Result5)), + {ok, 200, Result6} = request(get, uri(["authorization", "sources"]), []), + ?assertEqual([], get_sources(Result6)), ok. t_move_source(_) ->