From c26ec5c0ddd3f5be9920291f66a2ba285608398a Mon Sep 17 00:00:00 2001 From: zhanghongtong Date: Sat, 31 Jul 2021 11:44:50 +0800 Subject: [PATCH] feat(authz): support api Signed-off-by: zhanghongtong --- apps/emqx_authz/src/emqx_authz.erl | 46 ++++-- apps/emqx_authz/src/emqx_authz_api.erl | 154 +++++++++++------- apps/emqx_authz/src/emqx_authz_api_schema.erl | 144 ++++++++++++++++ 3 files changed, 268 insertions(+), 76 deletions(-) create mode 100644 apps/emqx_authz/src/emqx_authz_api_schema.erl diff --git a/apps/emqx_authz/src/emqx_authz.erl b/apps/emqx_authz/src/emqx_authz.erl index 50c8e5122..2599fd161 100644 --- a/apps/emqx_authz/src/emqx_authz.erl +++ b/apps/emqx_authz/src/emqx_authz.erl @@ -41,42 +41,56 @@ register_metrics() -> init() -> ok = register_metrics(), emqx_config_handler:add_handler(?CONF_KEY_PATH, ?MODULE), - NRules = [init_rule(Rule) || Rule <- lookup()], + NRules = [init_rule(Rule) || Rule <- emqx_config:get(?CONF_KEY_PATH, [])], ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [NRules]}, -1). lookup() -> - emqx_config:get(?CONF_KEY_PATH, []). + {_M, _F, A}= find_action_in_hooks(), + A. update(Cmd, Rules) -> emqx_config:update(emqx_authz_schema, ?CONF_KEY_PATH, {Cmd, Rules}). %% For now we only support re-creating the entire rule list -pre_config_update({head, Rule}, OldConf) when is_map(Rule), is_list(OldConf) -> - [Rule | OldConf]; -pre_config_update({tail, Rule}, OldConf) when is_map(Rule), is_list(OldConf) -> - OldConf ++ [Rule]; -pre_config_update({_, NewConf}, _OldConf) -> +pre_config_update({head, Rules}, OldConf) when is_list(Rules), is_list(OldConf) -> + Rules ++ OldConf; +pre_config_update({tail, Rules}, OldConf) when is_list(Rules), is_list(OldConf) -> + OldConf ++ Rules; +pre_config_update({_, Rules}, _OldConf) when is_list(Rules)-> %% overwrite the entire config! - case is_list(NewConf) of - true -> NewConf; - false -> [NewConf] - end. + Rules. post_config_update(_, undefined, _OldConf) -> - %_ = [release_rules(Rule) || Rule <- OldConf], ok; +post_config_update({head, Rules}, _NewRules, _OldConf) -> + InitedRules = [init_rule(Rule) || Rule <- check_rules(Rules)], + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:append(InitedRules, lookup())]}, -1), + ok = emqx_authz_cache:drain_cache(); +post_config_update({tail, Rules}, _NewRules, _OldConf) -> + InitedRules = [init_rule(Rule) || Rule <- check_rules(Rules)], + emqx_hooks:put('client.authorize', {?MODULE, authorize, [lists:append(InitedRules, lookup())]}, -1), + ok = emqx_authz_cache:drain_cache(); post_config_update(_, NewRules, _OldConf) -> - %_ = [release_rules(Rule) || Rule <- OldConf], + %% overwrite the entire config! + OldInitedRules = lookup(), InitedRules = [init_rule(Rule) || Rule <- NewRules], - Action = find_action_in_hooks(), - ok = emqx_hooks:del('client.authorize', Action), - ok = emqx_hooks:add('client.authorize', {?MODULE, authorize, [InitedRules]}, -1), + ok = emqx_hooks:put('client.authorize', {?MODULE, authorize, [InitedRules]}, -1), + lists:foreach(fun (#{type := _Type, enable := true, metadata := #{id := Id}}) -> + ok = emqx_resource:remove(Id); + (_) -> ok + end, OldInitedRules), ok = emqx_authz_cache:drain_cache(). %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- +check_rules(RawRules) -> + {ok, Conf} = hocon:binary(jsx:encode(#{<<"authorization">> => #{<<"rules">> => RawRules}}), #{format => richmap}), + CheckConf = hocon_schema:check(emqx_authz_schema, Conf, #{atom_key => true}), + #{authorization := #{rules := Rules}} = hocon_schema:richmap_to_map(CheckConf), + Rules. + find_action_in_hooks() -> Callbacks = emqx_hooks:lookup('client.authorize'), [Action] = [Action || {callback,{?MODULE, authorize, _} = Action, _, _} <- Callbacks ], diff --git a/apps/emqx_authz/src/emqx_authz_api.erl b/apps/emqx_authz/src/emqx_authz_api.erl index 974f72dbe..ad56d2573 100644 --- a/apps/emqx_authz/src/emqx_authz_api.erl +++ b/apps/emqx_authz/src/emqx_authz_api.erl @@ -16,74 +16,108 @@ -module(emqx_authz_api). +-behavior(minirest_api). + -include("emqx_authz.hrl"). --rest_api(#{name => lookup_authz, - method => 'GET', - path => "/authz", - func => lookup_authz, - descr => "Lookup Authorization" - }). +-define(EXAMPLE_RETURNED_RULES, + #{rules => [ #{principal => <<"all">>, + permission => <<"allow">>, + action => <<"all">>, + topics => [<<"#">>], + metadata => #{id => 1} + } + ] + }). --rest_api(#{name => update_authz, - method => 'PUT', - path => "/authz", - func => update_authz, - descr => "Rewrite authz list" - }). +-define(EXAMPLE_RULE1, #{principal => <<"all">>, + permission => <<"allow">>, + action => <<"all">>, + topics => [<<"#">>]}). --rest_api(#{name => append_authz, - method => 'POST', - path => "/authz/append", - func => append_authz, - descr => "Add a new rule at the end of the authz list" - }). - --rest_api(#{name => push_authz, - method => 'POST', - path => "/authz/push", - func => push_authz, - descr => "Add a new rule at the start of the authz list" - }). - --export([ lookup_authz/2 - , update_authz/2 - , append_authz/2 - , push_authz/2 +-export([ api_spec/0 + , authorization/2 ]). -lookup_authz(_Bindings, _Params) -> - return({ok, emqx_authz:lookup()}). +api_spec() -> + {[ authorization_api() + ], definitions()}. -update_authz(_Bindings, Params) -> - Rules = form_rules(Params), - return(emqx_authz:update(replace, Rules)). +definitions() -> emqx_authz_api_schema:definitions(). -append_authz(_Bindings, Params) -> - Rules = form_rules(Params), - return(emqx_authz:update(tail, Rules)). +authorization_api() -> + Metadata = #{ + get => #{ + description => "List authorization rules", + parameters => [], + responses => #{ + <<"200">> => #{ + description => <<"OK">>, + content => #{ + 'application/json' => #{ + schema => #{ + type => object, + required => [rules], + properties => #{rules => #{ + type => array, + items => minirest:ref(<<"returned_rules">>) + } + } + }, + examples => #{ + rules => #{ + summary => <<"Rules">>, + value => jsx:encode(?EXAMPLE_RETURNED_RULES) + } + } + } + } + }, + <<"404">> => #{description => <<"Not Found">>} + } + }, + post => #{ + description => "Add new rule", + requestBody => #{ + content => #{ + 'application/json' => #{ + schema => minirest:ref(<<"rules">>), + examples => #{ + simple_rule => #{ + summary => <<"Rules">>, + value => jsx:encode(?EXAMPLE_RULE1) + } + } + } + } + }, + responses => #{ + <<"201">> => #{description => <<"Created">>}, + <<"400">> => #{description => <<"Bad Request">>} + } + } + }, + {"/authorization", Metadata, authorization}. -push_authz(_Bindings, Params) -> - Rules = form_rules(Params), - return(emqx_authz:update(head, Rules)). - -%%------------------------------------------------------------------------------ -%% Interval Funcs -%%------------------------------------------------------------------------------ - -form_rules(Params) -> - Params. - -%%-------------------------------------------------------------------- -%% EUnits -%%-------------------------------------------------------------------- - --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). +authorization(get, _Request) -> + Rules = lists:foldl(fun (#{type := _Type, enable := true, metadata := #{id := Id} = MataData} = Rule, AccIn) -> + NRule = case emqx_resource:health_check(Id) of + ok -> + Rule#{metadata => MataData#{status => healthy}}; + _ -> + Rule#{metadata => MataData#{status => unhealthy}} + end, + lists:append(AccIn, [NRule]); + (Rule, AccIn) -> + lists:append(AccIn, [Rule]) + end, [], emqx_authz:lookup()), + {200, #{rules => [Rules]}}; +authorization(post, Request) -> + {ok, Body, _} = cowboy_req:read_body(Request), + RawConfig = jsx:decode(Body, [return_maps]), + case emqx_authz:update(head, [RawConfig]) of + ok -> {201}; + {error, Reason} -> {400, #{messgae => atom_to_binary(Reason)}} + end. --endif. - -return(_) -> -%% TODO: V5 api - ok. \ No newline at end of file diff --git a/apps/emqx_authz/src/emqx_authz_api_schema.erl b/apps/emqx_authz/src/emqx_authz_api_schema.erl new file mode 100644 index 000000000..851d7f26a --- /dev/null +++ b/apps/emqx_authz/src/emqx_authz_api_schema.erl @@ -0,0 +1,144 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-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_authz_api_schema). + +-export([definitions/0]). + +definitions() -> + RetruenedRules = #{ + allOf => [ #{type => object, + properties => #{ + annotations => #{ + type => object, + required => [id], + properties => #{ + id => #{ + type => string + }, + principal => minirest:ref(<<"principal">>) + } + + } + + } + } + , minirest:ref(<<"rules">>) + ] + }, + Rules = #{ + oneOf => [ minirest:ref(<<"simple_rule">>) + % , minirest:ref(<<"connector_redis">>) + ] + }, + % ConnectorRedis = #{ + % type => object, + % required => [principal, type, enable, config, cmd] + % properties => #{ + % principal => minirest:ref(<<"principal">>), + % type => #{ + % type => string, + % enum => [<<"redis">>], + % example => <<"redis">> + % }, + % enable => #{ + % type => boolean, + % example => true + % } + % config => #{ + % type => + % } + % } + % } + SimpleRule = #{ + type => object, + required => [principal, permission, action, topics], + properties => #{ + action => #{ + type => string, + enum => [<<"publish">>, <<"subscribe">>, <<"all">>], + example => <<"publish">> + }, + permission => #{ + type => string, + enum => [<<"allow">>, <<"deny">>], + example => <<"allow">> + }, + topics => #{ + type => array, + items => #{ + oneOf => [ #{type => string, example => <<"#">>} + , #{type => object, + required => [eq], + properties => #{ + eq => #{type => string} + }, + example => #{eq => <<"#">>} + } + ] + } + }, + principal => minirest:ref(<<"principal">>) + } + }, + Principal = #{ + oneOf => [ minirest:ref(<<"principal_username">>) + , minirest:ref(<<"principal_clientid">>) + , minirest:ref(<<"principal_ipaddress">>) + , #{type => string, enum=>[<<"all">>], example => <<"all">>} + , #{type => object, + required => ['and'], + properties => #{'and' => #{type => array, + items => #{oneOf => [ minirest:ref(<<"principal_username">>) + , minirest:ref(<<"principal_clientid">>) + , minirest:ref(<<"principal_ipaddress">>) + ]}}}, + example => #{'and' => [#{username => <<"emqx">>}, #{clientid => <<"emqx">>}]} + } + , #{type => object, + required => ['or'], + properties => #{'and' => #{type => array, + items => #{oneOf => [ minirest:ref(<<"principal_username">>) + , minirest:ref(<<"principal_clientid">>) + , minirest:ref(<<"principal_ipaddress">>) + ]}}}, + example => #{'or' => [#{username => <<"emqx">>}, #{clientid => <<"emqx">>}]} + } + ] + }, + PrincipalUsername = #{type => object, + required => [username], + properties => #{username => #{type => string}}, + example => #{username => <<"emqx">>} + }, + PrincipalClientid = #{type => object, + required => [clientid], + properties => #{clientid => #{type => string}}, + example => #{clientid => <<"emqx">>} + }, + PrincipalIpaddress = #{type => object, + required => [ipaddress], + properties => #{ipaddress => #{type => string}}, + example => #{ipaddress => <<"127.0.0.1">>} + }, + [ #{<<"returned_rules">> => RetruenedRules} + , #{<<"rules">> => Rules} + , #{<<"simple_rule">> => SimpleRule} + , #{<<"principal">> => Principal} + , #{<<"principal_username">> => PrincipalUsername} + , #{<<"principal_clientid">> => PrincipalClientid} + , #{<<"principal_ipaddress">> => PrincipalIpaddress} + ].