Parcourir la source

feat(authz api): support file type for sources

zhanghongtong il y a 4 ans
Parent
commit
07dcd9e705

+ 31 - 3
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}
     ].

+ 82 - 12
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/binary, "\n">> || 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/binary, "\n">> || 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/binary, "\n">> || 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} ->

+ 15 - 1
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()),

+ 12 - 2
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"]), []),