diff --git a/apps/emqx_dashboard/include/emqx_dashboard.hrl b/apps/emqx_dashboard/include/emqx_dashboard.hrl index 095229c53..a72f98b1f 100644 --- a/apps/emqx_dashboard/include/emqx_dashboard.hrl +++ b/apps/emqx_dashboard/include/emqx_dashboard.hrl @@ -15,23 +15,28 @@ %%-------------------------------------------------------------------- -define(ADMIN, emqx_admin). +-define(ROLE_VIEWER, <<"viewer">>). +-define(ROLE_DEFAULT, ?ROLE_VIEWER). +-define(ROLE_SUPERUSER, <<"superuser">>). + -record(?ADMIN, { username :: binary(), pwdhash :: binary(), description :: binary(), - role = undefined :: atom(), - %% not used so far, for future extension - extra = [] :: term() + role = ?ROLE_DEFAULT :: binary(), + extra = #{} :: map() }). +-type dashboard_user_role() :: binary(). +-type dashboard_user() :: #?ADMIN{}. + -define(ADMIN_JWT, emqx_admin_jwt). -record(?ADMIN_JWT, { token :: binary(), username :: binary(), exptime :: integer(), - %% not used so far, fur future extension - extra = [] :: term() + extra = #{} :: map() }). -define(TAB_COLLECT, emqx_collect). diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl index 8c56d8014..58d12118c 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard.erl @@ -205,13 +205,15 @@ authorize(Req) -> {basic, Username, Password} -> api_key_authorize(Req, Username, Password); {bearer, Token} -> - case emqx_dashboard_admin:verify_token(Token) of + case emqx_dashboard_admin:verify_token(Req, Token) of ok -> ok; {error, token_timeout} -> {401, 'TOKEN_TIME_OUT', <<"Token expired, get new token by POST /login">>}; {error, not_found} -> - {401, 'BAD_TOKEN', <<"Get a token by POST /login">>} + {401, 'BAD_TOKEN', <<"Get a token by POST /login">>}; + {error, unauthorized_role} -> + {401, 'UNAUTHORIZED_ROLE', <<"Unauthorized Role">>} end; _ -> return_unauthorized( diff --git a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl index e8f95d609..1318bce6b 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_admin.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_admin.erl @@ -30,10 +30,10 @@ -export([mnesia/1]). -export([ - add_user/3, - force_add_user/3, + add_user/4, + force_add_user/4, remove_user/1, - update_user/2, + update_user/3, lookup_user/1, change_password/2, change_password/3, @@ -43,7 +43,7 @@ -export([ sign_token/2, - verify_token/1, + verify_token/2, destroy_token_by_username/2 ]). -export([ @@ -98,18 +98,19 @@ add_default_user() -> %% API %%-------------------------------------------------------------------- --spec add_user(binary(), binary(), binary()) -> {ok, map()} | {error, any()}. -add_user(Username, Password, Desc) when +-spec add_user(binary(), binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, any()}. +add_user(Username, Password, Role, Desc) when is_binary(Username), is_binary(Password) -> - case {legal_username(Username), legal_password(Password)} of - {ok, ok} -> do_add_user(Username, Password, Desc); - {{error, Reason}, _} -> {error, Reason}; - {_, {error, Reason}} -> {error, Reason} + case {legal_username(Username), legal_password(Password), legal_role(Role)} of + {ok, ok, ok} -> do_add_user(Username, Password, Role, Desc); + {{error, Reason}, _, _} -> {error, Reason}; + {_, {error, Reason}, _} -> {error, Reason}; + {_, _, {error, Reason}} -> {error, Reason} end. -do_add_user(Username, Password, Desc) -> - Res = mria:transaction(?DASHBOARD_SHARD, fun add_user_/3, [Username, Password, Desc]), +do_add_user(Username, Password, Role, Desc) -> + Res = mria:transaction(?DASHBOARD_SHARD, fun add_user_/4, [Username, Password, Role, Desc]), return(Res). %% 0-9 or A-Z or a-z or $_ @@ -177,11 +178,12 @@ ascii_character_validate(Password) -> contain(Xs, Spec) -> lists:any(fun(X) -> lists:member(X, Spec) end, Xs). %% black-magic: force overwrite a user -force_add_user(Username, Password, Desc) -> +force_add_user(Username, Password, Role, Desc) -> AddFun = fun() -> mnesia:write(#?ADMIN{ username = Username, pwdhash = hash(Password), + role = Role, description = Desc }) end, @@ -191,12 +193,12 @@ force_add_user(Username, Password, Desc) -> end. %% @private -add_user_(Username, Password, Desc) -> +add_user_(Username, Password, Role, Desc) -> case mnesia:wread({?ADMIN, Username}) of [] -> Admin = #?ADMIN{username = Username, pwdhash = hash(Password), description = Desc}, mnesia:write(Admin), - #{username => Username, description => Desc}; + #{username => Username, role => Role, description => Desc}; [_] -> mnesia:abort(<<"username_already_exist">>) end. @@ -217,9 +219,14 @@ remove_user(Username) when is_binary(Username) -> {error, Reason} end. --spec update_user(binary(), binary()) -> {ok, map()} | {error, term()}. -update_user(Username, Desc) when is_binary(Username) -> - return(mria:transaction(?DASHBOARD_SHARD, fun update_user_/2, [Username, Desc])). +-spec update_user(binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, term()}. +update_user(Username, Role, Desc) when is_binary(Username) -> + case legal_role(Role) of + ok -> + return(mria:transaction(?DASHBOARD_SHARD, fun update_user_/3, [Username, Role, Desc])); + Error -> + Error + end. hash(Password) -> SaltBin = emqx_dashboard_token:salt(), @@ -240,18 +247,18 @@ sha256(SaltBin, Password) -> crypto:hash('sha256', <>). %% @private -update_user_(Username, Desc) -> +update_user_(Username, Role, Desc) -> case mnesia:wread({?ADMIN, Username}) of [] -> mnesia:abort(<<"username_not_found">>); [Admin] -> - mnesia:write(Admin#?ADMIN{description = Desc}), - #{username => Username, description => Desc} + mnesia:write(Admin#?ADMIN{role = Role, description = Desc}), + #{username => Username, role => Role, description => Desc} end. change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) -> case check(Username, OldPasswd) of - ok -> change_password(Username, NewPasswd); + {ok, _} -> change_password(Username, NewPasswd); Error -> Error end. @@ -320,9 +327,9 @@ check(_, undefined) -> {error, <<"password_not_provided">>}; check(Username, Password) -> case lookup_user(Username) of - [#?ADMIN{pwdhash = PwdHash}] -> + [#?ADMIN{pwdhash = PwdHash} = User] -> case verify_hash(Password, PwdHash) of - ok -> ok; + ok -> {ok, User}; error -> {error, <<"password_error">>} end; [] -> @@ -333,14 +340,14 @@ check(Username, Password) -> %% token sign_token(Username, Password) -> case check(Username, Password) of - ok -> - emqx_dashboard_token:sign(Username, Password); + {ok, User} -> + emqx_dashboard_token:sign(User, Password); Error -> Error end. -verify_token(Token) -> - emqx_dashboard_token:verify(Token). +verify_token(Req, Token) -> + emqx_dashboard_token:verify(Req, Token). destroy_token_by_username(Username, Token) -> case emqx_dashboard_token:lookup(Token) of @@ -363,10 +370,21 @@ add_default_user(Username, Password) when ?EMPTY_KEY(Username) orelse ?EMPTY_KEY {ok, empty}; add_default_user(Username, Password) -> case lookup_user(Username) of - [] -> do_add_user(Username, Password, <<"administrator">>); + [] -> do_add_user(Username, Password, ?ROLE_SUPERUSER, <<"administrator">>); _ -> {ok, default_user_exists} end. +-if(?EMQX_RELEASE_EDITION == ee). +legal_role(Role) -> + emqx_dashboard_rbac:legal_role(Role). + +-else. + +legal_role(_) -> + ok. + +-endif. + -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 108cde379..de2022b30 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -18,6 +18,7 @@ -behaviour(minirest_api). +-include("emqx_dashboard.hrl"). -include_lib("hocon/include/hoconsc.hrl"). -include_lib("emqx/include/logger.hrl"). -include_lib("typerefl/include/types.hrl"). @@ -111,9 +112,9 @@ schema("/users") -> post => #{ tags => [<<"dashboard">>], desc => ?DESC(create_user_api), - 'requestBody' => fields([username, password, description]), + 'requestBody' => fields([username, password, role, description]), responses => #{ - 200 => fields([username, description]) + 200 => fields([username, role, description]) } } }; @@ -124,9 +125,9 @@ schema("/users/:username") -> tags => [<<"dashboard">>], desc => ?DESC(update_user_api), parameters => fields([username_in_path]), - 'requestBody' => fields([description]), + 'requestBody' => fields([role, description]), responses => #{ - 200 => fields([username, description]), + 200 => fields([username, role, description]), 404 => response_schema(404) } }, @@ -170,7 +171,7 @@ response_schema(404) -> fields(user) -> fields([username, description]); fields(List) -> - [field(Key) || Key <- List]. + [field(Key) || Key <- List, field_filter(Key)]. field(username) -> {username, @@ -203,7 +204,9 @@ field(version) -> field(old_pwd) -> {old_pwd, mk(binary(), #{desc => ?DESC(old_pwd)})}; field(new_pwd) -> - {new_pwd, mk(binary(), #{desc => ?DESC(new_pwd)})}. + {new_pwd, mk(binary(), #{desc => ?DESC(new_pwd)})}; +field(role) -> + {role, mk(binary(), #{desc => ?DESC(role)})}. %% ------------------------------------------------------------------------------------------------- %% API @@ -242,16 +245,17 @@ users(get, _Request) -> {200, emqx_dashboard_admin:all_users()}; users(post, #{body := Params}) -> Desc = maps:get(<<"description">>, Params, <<"">>), + Role = maps:get(<<"role">>, Params, ?ROLE_DEFAULT), Username = maps:get(<<"username">>, Params), Password = maps:get(<<"password">>, Params), case ?EMPTY(Username) orelse ?EMPTY(Password) of true -> {400, ?BAD_REQUEST, <<"Username or password undefined">>}; false -> - case emqx_dashboard_admin:add_user(Username, Password, Desc) of + case emqx_dashboard_admin:add_user(Username, Password, Role, Desc) of {ok, Result} -> ?SLOG(info, #{msg => "Create dashboard success", username => Username}), - {200, Result}; + {200, filter_result(Result)}; {error, Reason} -> ?SLOG(info, #{ msg => "Create dashboard failed", @@ -263,12 +267,15 @@ users(post, #{body := Params}) -> end. user(put, #{bindings := #{username := Username}, body := Params}) -> + Role = maps:get(<<"role">>, Params, ?ROLE_DEFAULT), Desc = maps:get(<<"description">>, Params), - case emqx_dashboard_admin:update_user(Username, Desc) of + case emqx_dashboard_admin:update_user(Username, Role, Desc) of {ok, Result} -> - {200, Result}; + {200, filter_result(Result)}; + {error, <<"username_not_found">> = Reason} -> + {404, ?USER_NOT_FOUND, Reason}; {error, Reason} -> - {404, ?USER_NOT_FOUND, Reason} + {400, ?BAD_REQUEST, Reason} end; user(delete, #{bindings := #{username := Username}, headers := Headers}) -> case Username == emqx_dashboard_admin:default_username() of @@ -347,3 +354,22 @@ change_pwd(post, #{bindings := #{username := Username}, body := Params}) -> {400, ?BAD_REQUEST, Reason} end end. + +-if(?EMQX_RELEASE_EDITION == ee). +field_filter(_) -> + true. + +filter_result(Result) -> + Result. + +-else. + +field_filter(role) -> + false; +field_filter(_) -> + true. + +filter_result(Result) -> + maps:without([Role], Result). + +-endif. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_cli.erl b/apps/emqx_dashboard/src/emqx_dashboard_cli.erl index 52a915ecb..3da3e822d 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_cli.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_cli.erl @@ -16,6 +16,8 @@ -module(emqx_dashboard_cli). +-include("emqx_dashboard.hrl"). + -export([ load/0, admins/1, @@ -25,15 +27,6 @@ load() -> emqx_ctl:register_command(admins, {?MODULE, admins}, []). -admins(["add", Username, Password]) -> - admins(["add", Username, Password, ""]); -admins(["add", Username, Password, Desc]) -> - case emqx_dashboard_admin:add_user(bin(Username), bin(Password), bin(Desc)) of - {ok, _} -> - emqx_ctl:print("ok~n"); - {error, Reason} -> - print_error(Reason) - end; admins(["passwd", Username, Password]) -> case emqx_dashboard_admin:change_password(bin(Username), bin(Password)) of {ok, _} -> @@ -48,14 +41,8 @@ admins(["del", Username]) -> {error, Reason} -> print_error(Reason) end; -admins(_) -> - emqx_ctl:usage( - [ - {"admins add ", "Add dashboard user"}, - {"admins passwd ", "Reset dashboard user password"}, - {"admins del ", "Delete dashboard user"} - ] - ). +admins(Args) -> + inner_admins(Args). unload() -> emqx_ctl:unregister_command(admins). @@ -67,3 +54,47 @@ print_error(Reason) when is_binary(Reason) -> %% Maybe has more types of error, but there is only binary now. So close it for dialyzer. % print_error(Reason) -> % emqx_ctl:print("Error: ~p~n", [Reason]). + +-if(?EMQX_RELEASE_EDITION == ee). +usage() -> + [ + {"admins add ", "Add dashboard user"}, + {"admins passwd ", "Reset dashboard user password"}, + {"admins del ", "Delete dashboard user"} + ]. + +inner_admins(["add", Username, Password]) -> + inner_admins(["add", Username, Password, ?ROLE_SUPERUSER]); +inner_admins(["add", Username, Password, Role]) -> + inner_admins(["add", Username, Password, Role, ""]); +inner_admins(["add", Username, Password, Role, Desc]) -> + case emqx_dashboard_admin:add_user(bin(Username), bin(Password), bin(Role), bin(Desc)) of + {ok, _} -> + emqx_ctl:print("ok~n"); + {error, Reason} -> + print_error(Reason) + end; +inner_admins(_) -> + emqx_ctl:usage(usage()). +-else. + +usage() -> + [ + {"admins add ", "Add dashboard user"}, + {"admins passwd ", "Reset dashboard user password"}, + {"admins del ", "Delete dashboard user"} + ]. + +inner_admins(["add", Username, Password]) -> + inner_admins(["add", Username, Password, ""]); +inner_admins(["add", Username, Password, Desc]) -> + case emqx_dashboard_admin:add_user(bin(Username), bin(Password), ?ROLE_SUPERUSER, bin(Desc)) of + {ok, _} -> + emqx_ctl:print("ok~n"); + {error, Reason} -> + print_error(Reason) + end; +inner_admins(_) -> + emqx_ctl:usage(usage()). + +-endif. diff --git a/apps/emqx_dashboard/src/emqx_dashboard_token.erl b/apps/emqx_dashboard/src/emqx_dashboard_token.erl index e8357c458..d0e3b09f2 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_token.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_token.erl @@ -20,7 +20,7 @@ -export([ sign/2, - verify/1, + verify/2, lookup/1, owner/1, destroy/1, @@ -55,14 +55,17 @@ %%-------------------------------------------------------------------- %% jwt function --spec sign(Username :: binary(), Password :: binary()) -> +-spec sign(User :: dashboard_user(), Password :: binary()) -> {ok, Token :: binary()} | {error, Reason :: term()}. -sign(Username, Password) -> - do_sign(Username, Password). +sign(User, Password) -> + do_sign(User, Password). --spec verify(Token :: binary()) -> Result :: ok | {error, token_timeout | not_found}. -verify(Token) -> - do_verify(Token). +-spec verify(_, Token :: binary()) -> + Result :: + ok + | {error, token_timeout | not_found | unauthorized_role}. +verify(Req, Token) -> + do_verify(Req, Token). -spec destroy(KeyOrKeys :: list() | binary() | #?ADMIN_JWT{}) -> ok. destroy([]) -> @@ -101,7 +104,7 @@ mnesia(boot) -> %%-------------------------------------------------------------------- %% jwt apply -do_sign(Username, Password) -> +do_sign(#?ADMIN{username = Username, extra = Extra}, Password) -> ExpTime = jwt_expiration_time(), Salt = salt(), JWK = jwk(Username, Password, Salt), @@ -114,22 +117,27 @@ do_sign(Username, Password) -> }, Signed = jose_jwt:sign(JWK, JWS, JWT), {_, Token} = jose_jws:compact(Signed), - JWTRec = format(Token, Username, ExpTime), + JWTRec = format(Token, Username, ExpTime, Extra), _ = mria:transaction(?DASHBOARD_SHARD, fun mnesia:write/1, [JWTRec]), {ok, Token}. -do_verify(Token) -> +do_verify(Req, Token) -> case lookup(Token) of - {ok, JWT = #?ADMIN_JWT{exptime = ExpTime}} -> + {ok, JWT = #?ADMIN_JWT{exptime = ExpTime, extra = Extra}} -> case ExpTime > erlang:system_time(millisecond) of true -> - NewJWT = JWT#?ADMIN_JWT{exptime = jwt_expiration_time()}, - {atomic, Res} = mria:transaction( - ?DASHBOARD_SHARD, - fun mnesia:write/1, - [NewJWT] - ), - Res; + case check_rbac(Req, Extra) of + true -> + NewJWT = JWT#?ADMIN_JWT{exptime = jwt_expiration_time()}, + {atomic, Res} = mria:transaction( + ?DASHBOARD_SHARD, + fun mnesia:write/1, + [NewJWT] + ), + Res; + _ -> + {error, unauthorized_role} + end; _ -> {error, token_timeout} end; @@ -183,11 +191,12 @@ jwt_expiration_time() -> token_ttl() -> emqx_conf:get([dashboard, token_expired_time], ?EXPTIME). -format(Token, Username, ExpTime) -> +format(Token, Username, ExpTime, Extra) -> #?ADMIN_JWT{ token = Token, username = Username, - exptime = ExpTime + exptime = ExpTime, + extra = #{role => role(Extra)} }. %%-------------------------------------------------------------------- @@ -234,3 +243,22 @@ clean_expired_jwt(Now) -> fun() -> mnesia:select(?TAB, Spec) end ), ok = destroy(JWTList). + +-if(?EMQX_RELEASE_EDITION == ee). +check_rbac(Req, Extra) -> + emqx_dashboard_rbac:check_rbac(Req, Extra). + +role(Data) -> + emqx_dashboard_rbac:role(Data). + +-else. + +-dialyzer({nowarn_function, [check_rbac/2]}). + +check_rbac(_Req, _Extra) -> + true. + +role(_) -> + undefined. + +-endif. diff --git a/apps/emqx_dashboard_rbac/.gitignore b/apps/emqx_dashboard_rbac/.gitignore new file mode 100644 index 000000000..3b0d6b553 --- /dev/null +++ b/apps/emqx_dashboard_rbac/.gitignore @@ -0,0 +1,2 @@ +src/emqx_ldap_filter_lexer.erl +src/emqx_ldap_filter_parser.erl diff --git a/apps/emqx_dashboard_rbac/BSL.txt b/apps/emqx_dashboard_rbac/BSL.txt new file mode 100644 index 000000000..0acc0e696 --- /dev/null +++ b/apps/emqx_dashboard_rbac/BSL.txt @@ -0,0 +1,94 @@ +Business Source License 1.1 + +Licensor: Hangzhou EMQ Technologies Co., Ltd. +Licensed Work: EMQX Enterprise Edition + The Licensed Work is (c) 2023 + Hangzhou EMQ Technologies Co., Ltd. +Additional Use Grant: Students and educators are granted right to copy, + modify, and create derivative work for research + or education. +Change Date: 2027-02-01 +Change License: Apache License, Version 2.0 + +For information about alternative licensing arrangements for the Software, +please contact Licensor: https://www.emqx.com/en/contact + +Notice + +The Business Source License (this document, or the “License”) is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. +“Business Source License” is a trademark of MariaDB Corporation Ab. + +----------------------------------------------------------------------------- + +Business Source License 1.1 + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark “Business Source License”, +as long as you comply with the Covenants of Licensor below. + +Covenants of Licensor + +In consideration of the right to use this License’s text and the “Business +Source License” name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where “compatible” means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text “None”. + +3. To specify a Change Date. + +4. Not to modify this License in any other way. diff --git a/apps/emqx_dashboard_rbac/README.md b/apps/emqx_dashboard_rbac/README.md new file mode 100644 index 000000000..9d854d29d --- /dev/null +++ b/apps/emqx_dashboard_rbac/README.md @@ -0,0 +1,15 @@ +# Dashboard Role-Based Access Control + +RBAC (Role-Based Access Control) is a common access control model for managing user access to systems, applications or resources. +In the RBAC model, access permissions are assigned and managed based on user roles instead of being directly associated with individual users, +making management and usage simpler. + +This application houses the RBAC feature for Dashboard. + +## Contributing + +Please see our [contributing.md](../../CONTRIBUTING.md). + +## License + +See [APL](../../APL.txt). diff --git a/apps/emqx_dashboard_rbac/docker-ct b/apps/emqx_dashboard_rbac/docker-ct new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/apps/emqx_dashboard_rbac/docker-ct @@ -0,0 +1 @@ + diff --git a/apps/emqx_dashboard_rbac/rebar.config b/apps/emqx_dashboard_rbac/rebar.config new file mode 100644 index 000000000..03d877a31 --- /dev/null +++ b/apps/emqx_dashboard_rbac/rebar.config @@ -0,0 +1,6 @@ +%% -*- mode: erlang; -*- + +{erl_opts, [debug_info]}. +{deps, [ + {emqx_connector, {path, "../../apps/emqx_dashboard"}} +]}. diff --git a/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.app.src b/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.app.src new file mode 100644 index 000000000..190764e2f --- /dev/null +++ b/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.app.src @@ -0,0 +1,14 @@ +{application, emqx_dashboard_rbac, [ + {description, "EMQX Dashboard RBAC"}, + {vsn, "0.1.0"}, + {registered, []}, + {applications, [ + kernel, + stdlib, + emqx_dashboard + ]}, + {env, []}, + {modules, []}, + + {links, []} +]}. diff --git a/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl b/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl new file mode 100644 index 000000000..93cfd66d1 --- /dev/null +++ b/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl @@ -0,0 +1,44 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_dashboard_rbac). + +-include_lib("emqx_dashboard/include/emqx_dashboard.hrl"). + +-export([check_rbac/2, role/1, legal_role/1]). + +-dialyzer({nowarn_function, role/1}). +%%===================================================================== +%% API +check_rbac(Req, Extra) -> + Method = cowboy_req:method(Req), + Role = role(Extra), + check_rbac_with_method(Role, Method). + +role(#?ADMIN{role = undefined}) -> + ?ROLE_SUPERUSER; +role(#?ADMIN{role = Role}) -> + Role; +role([]) -> + ?ROLE_SUPERUSER; +role(#{role := Role}) -> + Role. + +legal_role(Role) -> + case lists:member(Role, role_list()) of + true -> + ok; + _ -> + {error, <<"Role does not exist">>} + end. +%% =================================================================== +check_rbac_with_method(?ROLE_SUPERUSER, _) -> + true; +check_rbac_with_method(?ROLE_VIEWER, <<"get">>) -> + true; +check_rbac_with_method(_, _) -> + false. + +role_list() -> + [?ROLE_VIEWER, ?ROLE_SUPERUSER].