diff --git a/apps/emqx_authz/include/emqx_authz.hrl b/apps/emqx_authz/include/emqx_authz.hrl index 89fb97f82..7b584b59b 100644 --- a/apps/emqx_authz/include/emqx_authz.hrl +++ b/apps/emqx_authz/include/emqx_authz.hrl @@ -1,3 +1,19 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2022 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. +%%-------------------------------------------------------------------- + -define(APP, emqx_authz). -define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= <<"allow">>) orelse @@ -8,6 +24,10 @@ (A =:= all) orelse (A =:= <<"all">>) )). +%% authz_mnesia +-define(ACL_TABLE, emqx_acl). + +%% authz_cmd -define(CMD_REPLACE, replace). -define(CMD_DELETE, delete). -define(CMD_PREPEND, prepend). @@ -23,6 +43,7 @@ -define(RE_PLACEHOLDER, "\\$\\{[a-z0-9_]+\\}"). +%% API examples -define(USERNAME_RULES_EXAMPLE, #{username => user1, rules => [ #{topic => <<"test/toopic/1">>, permission => <<"allow">>, diff --git a/apps/emqx_authz/src/emqx_authz_api_mnesia.erl b/apps/emqx_authz/src/emqx_authz_api_mnesia.erl index cd70a5340..430ebe76e 100644 --- a/apps/emqx_authz/src/emqx_authz_api_mnesia.erl +++ b/apps/emqx_authz/src/emqx_authz_api_mnesia.erl @@ -22,6 +22,8 @@ -include_lib("emqx/include/logger.hrl"). -include_lib("typerefl/include/types.hrl"). +-import(hoconsc, [mk/1, mk/2, ref/1, ref/2, array/1, enum/1]). + -define(FORMAT_USERNAME_FUN, {?MODULE, format_by_username}). -define(FORMAT_CLIENTID_FUN, {?MODULE, format_by_clientid}). @@ -68,178 +70,191 @@ paths() -> %%-------------------------------------------------------------------- schema("/authorization/sources/built-in-database/username") -> - #{ - 'operationId' => users, - get => #{ - tags => [<<"authorization">>], - description => <<"Show the list of record for username">>, - parameters => [ hoconsc:ref(emqx_dashboard_swagger, page) - , hoconsc:ref(emqx_dashboard_swagger, limit)], - responses => #{ - 200 => swagger_with_example( {username_response_data, ?TYPE_REF} - , {username, ?PAGE_QUERY_EXAMPLE}) + #{ 'operationId' => users + , get => + #{ tags => [<<"authorization">>] + , description => <<"Show the list of record for username">> + , parameters => + [ ref(emqx_dashboard_swagger, page) + , ref(emqx_dashboard_swagger, limit) + , { like_username + , mk( binary(), #{ in => query + , required => false + , desc => <<"Fuzzy search `username` as substring">>})} + ] + , responses => + #{ 200 => swagger_with_example( {username_response_data, ?TYPE_REF} + , {username, ?PAGE_QUERY_EXAMPLE}) } - }, - post => #{ - tags => [<<"authorization">>], - description => <<"Add new records for username">>, - 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_ARRAY} - , {username, ?POST_ARRAY_EXAMPLE}), - responses => #{ - 204 => <<"Created">>, - 400 => emqx_dashboard_swagger:error_codes( [?BAD_REQUEST] - , <<"Bad username or bad rule schema">>) + } + , post => + #{ tags => [<<"authorization">>] + , description => <<"Add new records for username">> + , 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_ARRAY} + , {username, ?POST_ARRAY_EXAMPLE}) + , responses => + #{ 204 => <<"Created">> + , 400 => emqx_dashboard_swagger:error_codes( + [?BAD_REQUEST], <<"Bad username or bad rule schema">>) } } }; schema("/authorization/sources/built-in-database/clientid") -> - #{ - 'operationId' => clients, - get => #{ - tags => [<<"authorization">>], - description => <<"Show the list of record for clientid">>, - parameters => [ hoconsc:ref(emqx_dashboard_swagger, page) - , hoconsc:ref(emqx_dashboard_swagger, limit)], - responses => #{ - 200 => swagger_with_example( {clientid_response_data, ?TYPE_REF} - , {clientid, ?PAGE_QUERY_EXAMPLE}) + #{ 'operationId' => clients + , get => + #{ tags => [<<"authorization">>] + , description => <<"Show the list of record for clientid">> + , parameters => + [ ref(emqx_dashboard_swagger, page) + , ref(emqx_dashboard_swagger, limit) + , { like_clientid + , mk( binary() + , #{ in => query + , required => false + , desc => <<"Fuzzy search `clientid` as substring">>}) + } + ] + , responses => + #{ 200 => swagger_with_example( {clientid_response_data, ?TYPE_REF} + , {clientid, ?PAGE_QUERY_EXAMPLE}) + } } - }, - post => #{ - tags => [<<"authorization">>], - description => <<"Add new records for clientid">>, - 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_ARRAY} - , {clientid, ?POST_ARRAY_EXAMPLE}), - responses => #{ - 204 => <<"Created">>, - 400 => emqx_dashboard_swagger:error_codes( [?BAD_REQUEST] - , <<"Bad clientid or bad rule schema">>) + , post => + #{ tags => [<<"authorization">>] + , description => <<"Add new records for clientid">> + , 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_ARRAY} + , {clientid, ?POST_ARRAY_EXAMPLE}) + , responses => + #{ 204 => <<"Created">> + , 400 => emqx_dashboard_swagger:error_codes( + [?BAD_REQUEST], <<"Bad clientid or bad rule schema">>) + } } - } - }; + }; schema("/authorization/sources/built-in-database/username/:username") -> - #{ - 'operationId' => user, - get => #{ - tags => [<<"authorization">>], - description => <<"Get record info for username">>, - parameters => [hoconsc:ref(username)], - responses => #{ - 200 => swagger_with_example( {rules_for_username, ?TYPE_REF} - , {username, ?PUT_MAP_EXAMPLE}), - 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>) + #{ 'operationId' => user + , get => + #{ tags => [<<"authorization">>] + , description => <<"Get record info for username">> + , parameters => [ref(username)] + , responses => + #{ 200 => swagger_with_example( {rules_for_username, ?TYPE_REF} + , {username, ?PUT_MAP_EXAMPLE}) + , 404 => emqx_dashboard_swagger:error_codes( + [?NOT_FOUND], <<"Not Found">>) + } } - }, - put => #{ - tags => [<<"authorization">>], - description => <<"Set record for username">>, - parameters => [hoconsc:ref(username)], - 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_REF} - , {username, ?PUT_MAP_EXAMPLE}), - responses => #{ - 204 => <<"Updated">>, - 400 => emqx_dashboard_swagger:error_codes( [?BAD_REQUEST] - , <<"Bad username or bad rule schema">>) + , put => + #{ tags => [<<"authorization">>] + , description => <<"Set record for username">> + , parameters => [ref(username)] + , 'requestBody' => swagger_with_example( {rules_for_username, ?TYPE_REF} + , {username, ?PUT_MAP_EXAMPLE}) + , responses => + #{ 204 => <<"Updated">> + , 400 => emqx_dashboard_swagger:error_codes( + [?BAD_REQUEST], <<"Bad username or bad rule schema">>) + } } - }, - delete => #{ - tags => [<<"authorization">>], - description => <<"Delete one record for username">>, - parameters => [hoconsc:ref(username)], - responses => #{ - 204 => <<"Deleted">>, - 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad username">>) + , delete => + #{ tags => [<<"authorization">>] + , description => <<"Delete one record for username">> + , parameters => [ref(username)] + , responses => + #{ 204 => <<"Deleted">> + , 400 => emqx_dashboard_swagger:error_codes( + [?BAD_REQUEST], <<"Bad username">>) + } } - } - }; + }; schema("/authorization/sources/built-in-database/clientid/:clientid") -> - #{ - 'operationId' => client, - get => #{ - tags => [<<"authorization">>], - description => <<"Get record info for clientid">>, - parameters => [hoconsc:ref(clientid)], - responses => #{ - 200 => swagger_with_example( {rules_for_clientid, ?TYPE_REF} - , {clientid, ?PUT_MAP_EXAMPLE}), - 404 => emqx_dashboard_swagger:error_codes([?NOT_FOUND], <<"Not Found">>) + #{ 'operationId' => client + , get => + #{ tags => [<<"authorization">>] + , description => <<"Get record info for clientid">> + , parameters => [ref(clientid)] + , responses => + #{ 200 => swagger_with_example( {rules_for_clientid, ?TYPE_REF} + , {clientid, ?PUT_MAP_EXAMPLE}) + , 404 => emqx_dashboard_swagger:error_codes( + [?NOT_FOUND], <<"Not Found">>) + } + }, + put => + #{ tags => [<<"authorization">>] + , description => <<"Set record for clientid">> + , parameters => [ref(clientid)] + , 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_REF} + , {clientid, ?PUT_MAP_EXAMPLE}) + , responses => + #{ 204 => <<"Updated">> + , 400 => emqx_dashboard_swagger:error_codes( + [?BAD_REQUEST], <<"Bad clientid or bad rule schema">>) + } } - }, - put => #{ - tags => [<<"authorization">>], - description => <<"Set record for clientid">>, - parameters => [hoconsc:ref(clientid)], - 'requestBody' => swagger_with_example( {rules_for_clientid, ?TYPE_REF} - , {clientid, ?PUT_MAP_EXAMPLE}), - responses => #{ - 204 => <<"Updated">>, - 400 => emqx_dashboard_swagger:error_codes( - [?BAD_REQUEST], <<"Bad clientid or bad rule schema">>) + , delete => + #{ tags => [<<"authorization">>] + , description => <<"Delete one record for clientid">> + , parameters => [ref(clientid)] + , responses => + #{ 204 => <<"Deleted">> + , 400 => emqx_dashboard_swagger:error_codes( + [?BAD_REQUEST], <<"Bad clientid">>) + } } - }, - delete => #{ - tags => [<<"authorization">>], - description => <<"Delete one record for clientid">>, - parameters => [hoconsc:ref(clientid)], - responses => #{ - 204 => <<"Deleted">>, - 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad clientid">>) - } - } - }; + }; schema("/authorization/sources/built-in-database/all") -> - #{ - 'operationId' => all, - get => #{ - tags => [<<"authorization">>], - description => <<"Show the list of rules for all">>, - responses => #{ - 200 => swagger_with_example({rules_for_all, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE}) + #{ 'operationId' => all + , get => + #{ tags => [<<"authorization">>] + , description => <<"Show the list of rules for all">> + , responses => + #{200 => swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE})} } - }, - put => #{ - tags => [<<"authorization">>], - description => <<"Set the list of rules for all">>, - 'requestBody' => - swagger_with_example({rules_for_all, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE}), - responses => #{ - 204 => <<"Created">>, - 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad rule schema">>) + , put => + #{ tags => [<<"authorization">>] + , description => <<"Set the list of rules for all">> + , 'requestBody' => + swagger_with_example({rules, ?TYPE_REF}, {all, ?PUT_MAP_EXAMPLE}) + , responses => + #{ 204 => <<"Created">> + , 400 => emqx_dashboard_swagger:error_codes( + [?BAD_REQUEST], <<"Bad rule schema">>) + } } - } - }; + }; schema("/authorization/sources/built-in-database/purge-all") -> - #{ - 'operationId' => purge, - delete => #{ - tags => [<<"authorization">>], - description => <<"Purge all records">>, - responses => #{ - 204 => <<"Deleted">>, - 400 => emqx_dashboard_swagger:error_codes([?BAD_REQUEST], <<"Bad Request">>) + #{ 'operationId' => purge + , delete => + #{ tags => [<<"authorization">>] + , description => <<"Purge all records">> + , responses => + #{ 204 => <<"Deleted">> + , 400 => emqx_dashboard_swagger:error_codes( + [?BAD_REQUEST], <<"Bad Request">>) + } } - } - }. + }. fields(rule_item) -> - [ {topic, hoconsc:mk(string(), + [ {topic, mk(string(), #{ required => true , desc => <<"Rule on specific topic">> , example => <<"test/topic/1">> })} - , {permission, hoconsc:mk(hoconsc:enum([allow, deny]), + , {permission, mk(enum([allow, deny]), #{ desc => <<"Permission">> , required => true , example => allow })} - , {action, hoconsc:mk(hoconsc:enum([publish, subscribe, all]), + , {action, mk(enum([publish, subscribe, all]), #{ required => true , example => publish , desc => <<"Authorized action">> })} ]; fields(clientid) -> - [ {clientid, hoconsc:mk(binary(), + [ {clientid, mk(binary(), #{ in => path , required => true , desc => <<"ClientID">> @@ -247,50 +262,49 @@ fields(clientid) -> })} ]; fields(username) -> - [ {username, hoconsc:mk(binary(), + [ {username, mk(binary(), #{ in => path , required => true , desc => <<"Username">> , example => <<"user1">>})} ]; fields(rules_for_username) -> - [ {rules, hoconsc:mk(hoconsc:array(hoconsc:ref(rule_item)), #{})} - ] ++ fields(username); + fields(rules) + ++ fields(username); fields(username_response_data) -> - [ {data, hoconsc:mk(hoconsc:array(hoconsc:ref(rules_for_username)), #{})} - , {meta, hoconsc:ref(meta)} + [ {data, mk(array(ref(rules_for_username)), #{})} + , {meta, ref(meta)} ]; fields(rules_for_clientid) -> - [ {rules, hoconsc:mk(hoconsc:array(hoconsc:ref(rule_item)), #{})} - ] ++ fields(clientid); + fields(rules) + ++ fields(clientid); fields(clientid_response_data) -> - [ {data, hoconsc:mk(hoconsc:array(hoconsc:ref(rules_for_clientid)), #{})} - , {meta, hoconsc:ref(meta)} - ]; -fields(rules_for_all) -> - [ {rules, hoconsc:mk(hoconsc:array(hoconsc:ref(rule_item)), #{})} + [ {data, mk(array(ref(rules_for_clientid)), #{})} + , {meta, ref(meta)} ]; +fields(rules) -> + [{rules, mk(array(ref(rule_item)))}]; fields(meta) -> emqx_dashboard_swagger:fields(page) ++ emqx_dashboard_swagger:fields(limit) - ++ [{count, hoconsc:mk(integer(), #{example => 1})}]. + ++ [{count, mk(integer(), #{example => 1})}]. %%-------------------------------------------------------------------- %% HTTP API %%-------------------------------------------------------------------- -users(get, #{query_string := PageParams}) -> +users(get, #{query_string := QString}) -> {Table, MatchSpec} = emqx_authz_mnesia:list_username_rules(), - {200, emqx_mgmt_api:paginate(Table, MatchSpec, PageParams, ?FORMAT_USERNAME_FUN)}; + {200, emqx_mgmt_api:paginate(Table, MatchSpec, QString, ?FORMAT_USERNAME_FUN)}; users(post, #{body := Body}) when is_list(Body) -> lists:foreach(fun(#{<<"username">> := Username, <<"rules">> := Rules}) -> emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)) end, Body), {204}. -clients(get, #{query_string := PageParams}) -> +clients(get, #{query_string := QueryString}) -> {Table, MatchSpec} = emqx_authz_mnesia:list_clientid_rules(), - {200, emqx_mgmt_api:paginate(Table, MatchSpec, PageParams, ?FORMAT_CLIENTID_FUN)}; + {200, emqx_mgmt_api:paginate(Table, MatchSpec, QueryString, ?FORMAT_CLIENTID_FUN)}; clients(post, #{body := Body}) when is_list(Body) -> lists:foreach(fun(#{<<"clientid">> := Clientid, <<"rules">> := Rules}) -> emqx_authz_mnesia:store_rules({clientid, Clientid}, format_rules(Rules)) @@ -402,8 +416,8 @@ atom(A) when is_atom(A) -> A. swagger_with_example({Ref, TypeP}, {_Name, _Type} = Example) -> emqx_dashboard_swagger:schema_with_examples( case TypeP of - ?TYPE_REF -> hoconsc:ref(?MODULE, Ref); - ?TYPE_ARRAY -> hoconsc:array(hoconsc:ref(?MODULE, Ref)) + ?TYPE_REF -> ref(?MODULE, Ref); + ?TYPE_ARRAY -> array(ref(?MODULE, Ref)) end, rules_example(Example)). diff --git a/apps/emqx_authz/src/emqx_authz_mnesia.erl b/apps/emqx_authz/src/emqx_authz_mnesia.erl index 470dcd871..bcb329202 100644 --- a/apps/emqx_authz/src/emqx_authz_mnesia.erl +++ b/apps/emqx_authz/src/emqx_authz_mnesia.erl @@ -20,9 +20,9 @@ -include_lib("stdlib/include/ms_transform.hrl"). -include_lib("emqx/include/logger.hrl"). --define(ACL_SHARDED, emqx_acl_sharded). +-include("emqx_authz.hrl"). --define(ACL_TABLE, emqx_acl). +-define(ACL_SHARDED, emqx_acl_sharded). %% To save some space, use an integer for label, 0 for 'all', {1, Username} and {2, ClientId}. -define(ACL_TABLE_ALL, 0). @@ -114,10 +114,12 @@ authorize(#{username := Username, %% Management API %%-------------------------------------------------------------------- +%% Init -spec(init_tables() -> ok). init_tables() -> ok = mria_rlog:wait_for_shards([?ACL_SHARDED], infinity). +%% @doc Update authz rules -spec(store_rules(who(), rules()) -> ok). store_rules({username, Username}, Rules) -> Record = #emqx_acl{who = {?ACL_TABLE_USERNAME, Username}, rules = normalize_rules(Rules)}, @@ -129,6 +131,7 @@ store_rules(all, Rules) -> Record = #emqx_acl{who = ?ACL_TABLE_ALL, rules = normalize_rules(Rules)}, mria:dirty_write(Record). +%% @doc Clean all authz rules for (username & clientid & all) -spec(purge_rules() -> ok). purge_rules() -> ok = lists:foreach( @@ -137,6 +140,7 @@ purge_rules() -> end, mnesia:dirty_all_keys(?ACL_TABLE)). +%% @doc Get one record -spec(get_rules(who()) -> {ok, rules()} | not_found). get_rules({username, Username}) -> do_get_rules({?ACL_TABLE_USERNAME, Username}); @@ -145,6 +149,7 @@ get_rules({clientid, Clientid}) -> get_rules(all) -> do_get_rules(?ACL_TABLE_ALL). +%% @doc Delete one record -spec(delete_rules(who()) -> ok). delete_rules({username, Username}) -> mria:dirty_delete(?ACL_TABLE, {?ACL_TABLE_USERNAME, Username});