434 lines
16 KiB
Erlang
434 lines
16 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% 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.
|
|
%%--------------------------------------------------------------------
|
|
|
|
-module(emqx_authz_api_mnesia).
|
|
|
|
-behaviour(minirest_api).
|
|
|
|
-include("emqx_authz.hrl").
|
|
-include_lib("emqx/include/logger.hrl").
|
|
-include_lib("typerefl/include/types.hrl").
|
|
|
|
-define(FORMAT_USERNAME_FUN, {?MODULE, format_by_username}).
|
|
-define(FORMAT_CLIENTID_FUN, {?MODULE, format_by_clientid}).
|
|
|
|
-export([ api_spec/0
|
|
, paths/0
|
|
, schema/1
|
|
, fields/1
|
|
]).
|
|
|
|
%% operation funs
|
|
-export([ users/2
|
|
, clients/2
|
|
, user/2
|
|
, client/2
|
|
, all/2
|
|
, purge/2
|
|
]).
|
|
|
|
-export([ format_by_username/1
|
|
, format_by_clientid/1]).
|
|
|
|
-define(BAD_REQUEST, 'BAD_REQUEST').
|
|
-define(NOT_FOUND, 'NOT_FOUND').
|
|
|
|
-define(TYPE_REF, ref).
|
|
-define(TYPE_ARRAY, array).
|
|
-define(PAGE_QUERY_EXAMPLE, example_in_data).
|
|
-define(PUT_MAP_EXAMPLE, in_put_requestBody).
|
|
-define(POST_ARRAY_EXAMPLE, in_post_requestBody).
|
|
|
|
api_spec() ->
|
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
|
|
|
|
paths() ->
|
|
[ "/authorization/sources/built-in-database/username"
|
|
, "/authorization/sources/built-in-database/clientid"
|
|
, "/authorization/sources/built-in-database/username/:username"
|
|
, "/authorization/sources/built-in-database/clientid/:clientid"
|
|
, "/authorization/sources/built-in-database/all"
|
|
, "/authorization/sources/built-in-database/purge-all"].
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Schema for each URI
|
|
%%--------------------------------------------------------------------
|
|
|
|
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})
|
|
}
|
|
},
|
|
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})
|
|
}
|
|
},
|
|
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">>)
|
|
}
|
|
},
|
|
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">>)
|
|
}
|
|
},
|
|
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">>)
|
|
}
|
|
}
|
|
};
|
|
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">>)
|
|
}
|
|
},
|
|
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 => [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})
|
|
}
|
|
},
|
|
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">>)
|
|
}
|
|
}
|
|
};
|
|
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">>)
|
|
}
|
|
}
|
|
}.
|
|
|
|
fields(rule_item) ->
|
|
[ {topic, hoconsc:mk(string(),
|
|
#{ required => true
|
|
, desc => <<"Rule on specific topic">>
|
|
, example => <<"test/topic/1">>
|
|
})}
|
|
, {permission, hoconsc:mk(hoconsc:enum([allow, deny]),
|
|
#{ desc => <<"Permission">>
|
|
, required => true
|
|
, example => allow
|
|
})}
|
|
, {action, hoconsc:mk(hoconsc:enum([publish, subscribe, all]),
|
|
#{ required => true
|
|
, example => publish
|
|
, desc => <<"Authorized action">>
|
|
})}
|
|
];
|
|
fields(clientid) ->
|
|
[ {clientid, hoconsc:mk(binary(),
|
|
#{ in => path
|
|
, required => true
|
|
, desc => <<"ClientID">>
|
|
, example => <<"client1">>
|
|
})}
|
|
];
|
|
fields(username) ->
|
|
[ {username, hoconsc: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(username_response_data) ->
|
|
[ {data, hoconsc:mk(hoconsc:array(hoconsc:ref(rules_for_username)), #{})}
|
|
, {meta, hoconsc:ref(meta)}
|
|
];
|
|
fields(rules_for_clientid) ->
|
|
[ {rules, hoconsc:mk(hoconsc:array(hoconsc:ref(rule_item)), #{})}
|
|
] ++ 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)), #{})}
|
|
];
|
|
fields(meta) ->
|
|
emqx_dashboard_swagger:fields(page)
|
|
++ emqx_dashboard_swagger:fields(limit)
|
|
++ [{count, hoconsc:mk(integer(), #{example => 1})}].
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% HTTP API
|
|
%%--------------------------------------------------------------------
|
|
|
|
users(get, #{query_string := PageParams}) ->
|
|
{Table, MatchSpec} = emqx_authz_mnesia:list_username_rules(),
|
|
{200, emqx_mgmt_api:paginate(Table, MatchSpec, PageParams, ?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}) ->
|
|
{Table, MatchSpec} = emqx_authz_mnesia:list_clientid_rules(),
|
|
{200, emqx_mgmt_api:paginate(Table, MatchSpec, PageParams, ?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))
|
|
end, Body),
|
|
{204}.
|
|
|
|
user(get, #{bindings := #{username := Username}}) ->
|
|
case emqx_authz_mnesia:get_rules({username, Username}) of
|
|
not_found -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
|
|
{ok, Rules} ->
|
|
{200, #{username => Username,
|
|
rules => [ #{topic => Topic,
|
|
action => Action,
|
|
permission => Permission
|
|
} || {Permission, Action, Topic} <- Rules]}
|
|
}
|
|
end;
|
|
user(put, #{bindings := #{username := Username},
|
|
body := #{<<"username">> := Username, <<"rules">> := Rules}}) ->
|
|
emqx_authz_mnesia:store_rules({username, Username}, format_rules(Rules)),
|
|
{204};
|
|
user(delete, #{bindings := #{username := Username}}) ->
|
|
emqx_authz_mnesia:delete_rules({username, Username}),
|
|
{204}.
|
|
|
|
client(get, #{bindings := #{clientid := Clientid}}) ->
|
|
case emqx_authz_mnesia:get_rules({clientid, Clientid}) of
|
|
not_found -> {404, #{code => <<"NOT_FOUND">>, message => <<"Not Found">>}};
|
|
{ok, Rules} ->
|
|
{200, #{clientid => Clientid,
|
|
rules => [ #{topic => Topic,
|
|
action => Action,
|
|
permission => Permission
|
|
} || {Permission, Action, Topic} <- Rules]}
|
|
}
|
|
end;
|
|
client(put, #{bindings := #{clientid := Clientid},
|
|
body := #{<<"clientid">> := Clientid, <<"rules">> := Rules}}) ->
|
|
emqx_authz_mnesia:store_rules({clientid, Clientid}, format_rules(Rules)),
|
|
{204};
|
|
client(delete, #{bindings := #{clientid := Clientid}}) ->
|
|
emqx_authz_mnesia:delete_rules({clientid, Clientid}),
|
|
{204}.
|
|
|
|
all(get, _) ->
|
|
case emqx_authz_mnesia:get_rules(all) of
|
|
not_found ->
|
|
{200, #{rules => []}};
|
|
{ok, Rules} ->
|
|
{200, #{rules => [ #{topic => Topic,
|
|
action => Action,
|
|
permission => Permission
|
|
} || {Permission, Action, Topic} <- Rules]}
|
|
}
|
|
end;
|
|
all(put, #{body := #{<<"rules">> := Rules}}) ->
|
|
emqx_authz_mnesia:store_rules(all, format_rules(Rules)),
|
|
{204}.
|
|
|
|
purge(delete, _) ->
|
|
case emqx_authz_api_sources:get_raw_source(<<"built-in-database">>) of
|
|
[#{<<"enable">> := false}] ->
|
|
ok = emqx_authz_mnesia:purge_rules(),
|
|
{204};
|
|
[#{<<"enable">> := true}] ->
|
|
{400, #{code => <<"BAD_REQUEST">>,
|
|
message =>
|
|
<<"'built-in-database' type source must be disabled before purge.">>}};
|
|
[] ->
|
|
{404, #{code => <<"BAD_REQUEST">>,
|
|
message => <<"'built-in-database' type source is not found.">>
|
|
}}
|
|
end.
|
|
|
|
format_rules(Rules) when is_list(Rules) ->
|
|
lists:foldl(fun(#{<<"topic">> := Topic,
|
|
<<"action">> := Action,
|
|
<<"permission">> := Permission
|
|
}, AccIn) when ?PUBSUB(Action)
|
|
andalso ?ALLOW_DENY(Permission) ->
|
|
AccIn ++ [{ atom(Permission), atom(Action), Topic }]
|
|
end, [], Rules).
|
|
|
|
format_by_username([{username, Username}, {rules, Rules}]) ->
|
|
#{username => Username,
|
|
rules => [ #{topic => Topic,
|
|
action => Action,
|
|
permission => Permission
|
|
} || {Permission, Action, Topic} <- Rules]
|
|
}.
|
|
format_by_clientid([{clientid, Clientid}, {rules, Rules}]) ->
|
|
#{clientid => Clientid,
|
|
rules => [ #{topic => Topic,
|
|
action => Action,
|
|
permission => Permission
|
|
} || {Permission, Action, Topic} <- Rules]
|
|
}.
|
|
atom(B) when is_binary(B) ->
|
|
try binary_to_existing_atom(B, utf8)
|
|
catch
|
|
_Error:_Expection -> binary_to_atom(B)
|
|
end;
|
|
atom(A) when is_atom(A) -> A.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Internal functions
|
|
%%--------------------------------------------------------------------
|
|
|
|
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))
|
|
end,
|
|
rules_example(Example)).
|
|
|
|
rules_example({ExampleName, ExampleType}) ->
|
|
{Summary, Example} =
|
|
case ExampleName of
|
|
username -> {<<"Username">>, ?USERNAME_RULES_EXAMPLE};
|
|
clientid -> {<<"ClientID">>, ?CLIENTID_RULES_EXAMPLE};
|
|
all -> {<<"All">>, ?ALL_RULES_EXAMPLE}
|
|
end,
|
|
Value =
|
|
case ExampleType of
|
|
?PAGE_QUERY_EXAMPLE -> #{
|
|
data => [Example],
|
|
meta => ?META_EXAMPLE
|
|
};
|
|
?PUT_MAP_EXAMPLE ->
|
|
Example;
|
|
?POST_ARRAY_EXAMPLE ->
|
|
[Example]
|
|
end,
|
|
#{
|
|
'password-based:built-in-database' => #{
|
|
summary => Summary,
|
|
value => Value
|
|
}
|
|
}.
|