feat(authz): support api

Signed-off-by: zhanghongtong <rory-z@outlook.com>
This commit is contained in:
zhanghongtong 2021-07-31 11:44:50 +08:00 committed by Rory Z
parent ae8c3cf779
commit c26ec5c0dd
3 changed files with 268 additions and 76 deletions

View File

@ -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 ],

View File

@ -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.

View File

@ -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}
].