From 07dcd9e7052b6901d114348ff6a0964655d61a66 Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Thu, 2 Sep 2021 09:57:26 +0800 Subject: [PATCH] feat(authz api): support file type for sources --- apps/emqx_authz/src/emqx_authz_api_schema.erl | 34 ++++++- .../emqx_authz/src/emqx_authz_api_sources.erl | 94 ++++++++++++++++--- apps/emqx_authz/test/emqx_authz_SUITE.erl | 16 +++- .../test/emqx_authz_api_sources_SUITE.erl | 14 ++- 4 files changed, 140 insertions(+), 18 deletions(-) diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl index 7d8b583ab..18a5e2b18 100644 --- a/apps/emqx_authz/src/emqx_authz_api_schema.erl +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -41,7 +41,8 @@ definitions() -> ] }, Sources = #{ - oneOf => [ minirest:ref(<<"connector_redis">>) + oneOf => [ minirest:ref(<<"redis">>) + , minirest:ref(<<"file">>) ] }, SSL = #{ @@ -55,7 +56,7 @@ definitions() -> verify => #{type => boolean, example => false} } }, - ConnectorRedis = #{ + Redis = #{ type => object, required => [type, enable, config, cmd], properties => #{ @@ -123,8 +124,35 @@ definitions() -> } } }, + File = #{ + type => object, + required => [type, enable, rules], + properties => #{ + type => #{ + type => string, + enum => [<<"redis">>], + example => <<"redis">> + }, + enable => #{ + type => boolean, + example => true + }, + rules => #{ + type => array, + items => #{ + type => string, + example => <<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">> + } + }, + path => #{ + type => string, + example => <<"/path/to/authorizaiton_rules.conf">> + } + } + }, [ #{<<"returned_sources">> => RetruenedSources} , #{<<"sources">> => Sources} , #{<<"ssl">> => SSL} - , #{<<"connector_redis">> => ConnectorRedis} + , #{<<"redis">> => Redis} + , #{<<"file">> => File} ]. diff --git a/apps/emqx_authz/src/emqx_authz_api_sources.erl b/apps/emqx_authz/src/emqx_authz_api_sources.erl index a735d5d70..0ebf5e511 100644 --- a/apps/emqx_authz/src/emqx_authz_api_sources.erl +++ b/apps/emqx_authz/src/emqx_authz_api_sources.erl @@ -23,18 +23,30 @@ -define(EXAMPLE_REDIS, #{type=> redis, + enable => true, config => #{server => <<"127.0.0.1:3306">>, redis_type => single, pool_size => 1, auto_reconnect => true }, cmd => <<"HGETALL mqtt_authz">>}). +-define(EXAMPLE_FILE, + #{type=> file, + enable => true, + rules => [<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">>, + <<"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">> + ]}). + -define(EXAMPLE_RETURNED_REDIS, maps:put(annotations, #{status => healthy}, ?EXAMPLE_REDIS) ). +-define(EXAMPLE_RETURNED_FILE, + maps:put(annotations, #{status => healthy}, ?EXAMPLE_FILE) + ). -define(EXAMPLE_RETURNED, - #{sources => [?EXAMPLE_RETURNED_REDIS + #{sources => [ ?EXAMPLE_RETURNED_REDIS + , ?EXAMPLE_RETURNED_FILE ] }). @@ -91,6 +103,10 @@ sources_api() -> redis => #{ summary => <<"Redis">>, value => jsx:encode(?EXAMPLE_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_FILE) } } } @@ -113,7 +129,11 @@ sources_api() -> examples => #{ redis => #{ summary => <<"Redis">>, - value => jsx:encode([?EXAMPLE_REDIS]) + value => jsx:encode(?EXAMPLE_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_FILE) } } } @@ -148,9 +168,13 @@ source_api() -> 'application/json' => #{ schema => minirest:ref(<<"returned_sources">>), examples => #{ - sources => #{ - summary => <<"Sources">>, + redis => #{ + summary => <<"Redis">>, value => jsx:encode(?EXAMPLE_RETURNED_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_RETURNED_FILE) } } } @@ -179,6 +203,10 @@ source_api() -> redis => #{ summary => <<"Redis">>, value => jsx:encode(?EXAMPLE_REDIS) + }, + file => #{ + summary => <<"File">>, + value => jsx:encode(?EXAMPLE_FILE) } } } @@ -271,7 +299,16 @@ move_source_api() -> {"/authorization/sources/:type/move", Metadata, move_source}. sources(get, _) -> - Sources = lists:foldl(fun (#{type := _Type, enable := true, config := Config, annotations := #{id := Id}} = Source, AccIn) -> + Sources = lists:foldl(fun (#{enable := false} = Source, AccIn) -> + lists:append(AccIn, [Source#{annotations => #{status => unhealthy}}]); + (#{type := file, path := Path}, AccIn) -> + {ok, Rules} = file:consult(Path), + lists:append(AccIn, [#{type => file, + enable => true, + rules => [ io_lib:format("~p", [R])|| R <- Rules], + annotations => #{status => healthy} + }]); + (#{type := _Type, config := Config, annotations := #{id := Id}} = Source, AccIn) -> NSource0 = case maps:get(server, Config, undefined) of undefined -> Source; Server -> @@ -293,15 +330,33 @@ sources(get, _) -> lists:append(AccIn, [Source#{annotations => #{status => healthy}}]) end, [], emqx_authz:lookup()), {200, #{sources => Sources}}; -sources(post, #{body := Body}) -> +sources(post, #{body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) when is_list(Rules) -> + Filename = filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + write_file(Filename, erlang:list_to_bitstring([<> || Rule <- Rules])), + case emqx_authz:update(head, [#{type => file, enable => Enable, path => Filename}]) of + {ok, _} -> {204}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end; +sources(post, #{body := Body}) when is_map(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 := Body}) -> - case emqx_authz:update(replace, save_cert(Body)) of +sources(put, #{body := Body}) when is_list(Body) -> + NBody = [ begin + case Source of + #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable} -> + Filename = filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]), + write_file(Filename, erlang:list_to_bitstring([<> || Rule <- Rules])), + #{type => file, enable => Enable, path => Filename}; + _ -> save_cert(Source) + end + end || Source <- Body], + case emqx_authz:update(replace, NBody) of {ok, _} -> {204}; {error, Reason} -> {400, #{code => <<"BAD_REQUEST">>, @@ -311,8 +366,15 @@ sources(put, #{body := Body}) -> source(get, #{bindings := #{type := Type}}) -> case emqx_authz:lookup(Type) of {error, Reason} -> {404, #{messgae => atom_to_binary(Reason)}}; - #{enable := false} = Source -> {200, Source}; - #{type := file} = Source -> {200, Source}; + #{enable := false} = Source -> {200, Source#{annotations => #{status => unhealthy}}}; + #{type := file, path := Path}-> + {ok, Rules} = file:consult(Path), + {200, #{type => file, + enable => true, + rules => Rules, + annotations => #{status => healthy} + } + }; #{config := Config, annotations := #{id := Id}} = Source -> NSource0 = case maps:get(server, Config, undefined) of undefined -> Source; @@ -332,8 +394,16 @@ source(get, #{bindings := #{type := Type}}) -> end, {200, NSource2} end; -source(put, #{bindings := #{type := Type}, body := Body}) -> - +source(put, #{bindings := #{type := file}, body := #{<<"type">> := <<"file">>, <<"rules">> := Rules, <<"enable">> := Enable}}) -> + #{path := Path} = emqx_authz:lookup(file), + write_file(Path, erlang:list_to_bitstring([<> || Rule <- Rules])), + case emqx_authz:update({replace_once, file}, #{type => file, enable => Enable, path => Path}) of + {ok, _} -> {204}; + {error, Reason} -> + {400, #{code => <<"BAD_REQUEST">>, + messgae => atom_to_binary(Reason)}} + end; +source(put, #{bindings := #{type := Type}, body := Body}) when is_map(Body) -> case emqx_authz:update({replace_once, Type}, save_cert(Body)) of {ok, _} -> {204}; {error, not_found_source} -> diff --git a/apps/emqx_authz/test/emqx_authz_SUITE.erl b/apps/emqx_authz/test/emqx_authz_SUITE.erl index cee83cd30..88a0a9bf3 100644 --- a/apps/emqx_authz/test/emqx_authz_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_SUITE.erl @@ -114,6 +114,11 @@ init_per_testcase(_, Config) -> <<"ssl">> => #{<<"enable">> => false}}, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). +-define(SOURCE6, #{<<"type">> => <<"file">>, + <<"enable">> => true, + <<"path">> => emqx_ct_helpers:deps_path(emqx_authz, "etc/authorization_rules.conf") + }). + %%------------------------------------------------------------------------------ %% Testcases @@ -125,12 +130,14 @@ t_update_source(_) -> {ok, _} = emqx_authz:update(head, [?SOURCE1]), {ok, _} = emqx_authz:update(tail, [?SOURCE4]), {ok, _} = emqx_authz:update(tail, [?SOURCE5]), + {ok, _} = emqx_authz:update(tail, [?SOURCE6]), ?assertMatch([ #{type := http, enable := true} , #{type := mongo, enable := true} , #{type := mysql, enable := true} , #{type := pgsql, enable := true} , #{type := redis, enable := true} + , #{type := file, enable := true} ], emqx:get_config([authorization, sources], [])), {ok, _} = emqx_authz:update({replace_once, http}, ?SOURCE1#{<<"enable">> := false}), @@ -138,23 +145,26 @@ t_update_source(_) -> {ok, _} = emqx_authz:update({replace_once, mysql}, ?SOURCE3#{<<"enable">> := false}), {ok, _} = emqx_authz:update({replace_once, pgsql}, ?SOURCE4#{<<"enable">> := false}), {ok, _} = emqx_authz:update({replace_once, redis}, ?SOURCE5#{<<"enable">> := false}), + {ok, _} = emqx_authz:update({replace_once, file}, ?SOURCE6#{<<"enable">> := false}), ?assertMatch([ #{type := http, enable := false} , #{type := mongo, enable := false} , #{type := mysql, enable := false} , #{type := pgsql, enable := false} , #{type := redis, enable := false} + , #{type := file, enable := false} ], emqx:get_config([authorization, sources], [])), {ok, _} = emqx_authz:update(replace, []). t_move_source(_) -> - {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5]), + {ok, _} = emqx_authz:update(replace, [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), ?assertMatch([ #{type := http} , #{type := mongo} , #{type := mysql} , #{type := pgsql} , #{type := redis} + , #{type := file} ], emqx_authz:lookup()), {ok, _} = emqx_authz:move(pgsql, <<"top">>), @@ -163,6 +173,7 @@ t_move_source(_) -> , #{type := mongo} , #{type := mysql} , #{type := redis} + , #{type := file} ], emqx_authz:lookup()), {ok, _} = emqx_authz:move(http, <<"bottom">>), @@ -170,6 +181,7 @@ t_move_source(_) -> , #{type := mongo} , #{type := mysql} , #{type := redis} + , #{type := file} , #{type := http} ], emqx_authz:lookup()), @@ -178,6 +190,7 @@ t_move_source(_) -> , #{type := pgsql} , #{type := mongo} , #{type := redis} + , #{type := file} , #{type := http} ], emqx_authz:lookup()), @@ -185,6 +198,7 @@ t_move_source(_) -> ?assertMatch([ #{type := mysql} , #{type := pgsql} , #{type := redis} + , #{type := file} , #{type := http} , #{type := mongo} ], emqx_authz:lookup()), 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 ed3cf18d0..a7e747069 100644 --- a/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl +++ b/apps/emqx_authz/test/emqx_authz_api_sources_SUITE.erl @@ -96,6 +96,13 @@ }, <<"cmd">> => <<"HGETALL mqtt_authz:%u">> }). +-define(SOURCE6, #{<<"type">> => <<"file">>, + <<"enable">> => true, + <<"rules">> => + [<<"{allow,{username,\"^dashboard?\"},subscribe,[\"$SYS/#\"]}.">>, + <<"{allow,{ipaddr,\"127.0.0.1\"},all,[\"$SYS/#\",\"#\"]}.">> + ] + }). all() -> emqx_ct:all(?MODULE). @@ -119,7 +126,7 @@ init_per_suite(Config) -> ok = emqx_config:init_load(emqx_authz_schema, ?CONF_DEFAULT), - ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), + ok = emqx_ct_helpers:start_apps([emqx_authz, emqx_dashboard], fun set_special_configs/1), {ok, _} = emqx:update_config([authorization, cache, enable], false), {ok, _} = emqx:update_config([authorization, no_match], deny), @@ -183,7 +190,7 @@ t_api(_) -> {ok, 200, Result2} = request(get, uri(["authorization", "sources"]), []), ?assertEqual(20, length(get_sources(Result2))), - {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4]), + {ok, 204, _} = request(put, uri(["authorization", "sources"]), [?SOURCE1, ?SOURCE2, ?SOURCE3, ?SOURCE4, ?SOURCE5, ?SOURCE6]), {ok, 200, Result3} = request(get, uri(["authorization", "sources"]), []), Sources = get_sources(Result3), @@ -191,7 +198,10 @@ t_api(_) -> , #{<<"type">> := <<"mongo">>} , #{<<"type">> := <<"mysql">>} , #{<<"type">> := <<"pgsql">>} + , #{<<"type">> := <<"redis">>} + , #{<<"type">> := <<"file">>} ], Sources), + ?assert(filelib:is_file(filename:join([emqx:get_config([node, data_dir]), "authorization_rules.conf"]))), {ok, 204, _} = request(put, uri(["authorization", "sources", "http"]), ?SOURCE1#{<<"enable">> := false}), {ok, 200, Result4} = request(get, uri(["authorization", "sources", "http"]), []),