diff --git a/apps/emqx_bridge_kafka/test/emqx_bridge_kafka_impl_producer_SUITE.erl b/apps/emqx_bridge_kafka/test/emqx_bridge_kafka_impl_producer_SUITE.erl index 1dd5adab2..4450030e0 100644 --- a/apps/emqx_bridge_kafka/test/emqx_bridge_kafka_impl_producer_SUITE.erl +++ b/apps/emqx_bridge_kafka/test/emqx_bridge_kafka_impl_producer_SUITE.erl @@ -1260,7 +1260,7 @@ auth_header_() -> auth_header_(<<"admin">>, <<"public">>). auth_header_(Username, Password) -> - {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), + {ok, _Role, Token} = emqx_dashboard_admin:sign_token(Username, Password), {"Authorization", "Bearer " ++ binary_to_list(Token)}. api_path(Parts) -> diff --git a/apps/emqx_dashboard/src/emqx_dashboard_api.erl b/apps/emqx_dashboard/src/emqx_dashboard_api.erl index 029e35e3b..70d6accb4 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_api.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_api.erl @@ -77,7 +77,7 @@ schema("/login") -> summary => <<"Dashboard authentication">>, 'requestBody' => fields([username, password]), responses => #{ - 200 => fields([token, version, license]), + 200 => fields([role, token, version, license]), 401 => response_schema(401) }, security => [] @@ -219,14 +219,16 @@ login(post, #{body := Params}) -> Username = maps:get(<<"username">>, Params), Password = maps:get(<<"password">>, Params), case emqx_dashboard_admin:sign_token(Username, Password) of - {ok, Token} -> + {ok, Role, Token} -> ?SLOG(info, #{msg => "dashboard_login_successful", username => Username}), Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), - {200, #{ - token => Token, - version => Version, - license => #{edition => emqx_release:edition()} - }}; + {200, + filter_result(#{ + role => Role, + token => Token, + version => Version, + license => #{edition => emqx_release:edition()} + })}; {error, R} -> ?SLOG(info, #{msg => "dashboard_login_failed", username => Username, reason => R}), {401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>} diff --git a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl index a86c30893..75e93fdd1 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_swagger.erl @@ -28,7 +28,7 @@ -export([error_codes/1, error_codes/2]). -export([file_schema/1]). -export([base_path/0]). --export([relative_uri/1]). +-export([relative_uri/1, get_relative_uri/1]). -export([compose_filters/2]). -export([ @@ -212,6 +212,12 @@ base_path() -> relative_uri(Uri) -> base_path() ++ Uri. +-spec get_relative_uri(uri_string:uri_string()) -> {ok, uri_string:uri_string()} | error. +get_relative_uri(<>) -> + {ok, Path}; +get_relative_uri(_Path) -> + error. + file_schema(FileName) -> #{ content => #{ diff --git a/apps/emqx_dashboard/src/emqx_dashboard_token.erl b/apps/emqx_dashboard/src/emqx_dashboard_token.erl index 38856e7c7..866da971b 100644 --- a/apps/emqx_dashboard/src/emqx_dashboard_token.erl +++ b/apps/emqx_dashboard/src/emqx_dashboard_token.erl @@ -56,7 +56,7 @@ %%-------------------------------------------------------------------- %% jwt function -spec sign(User :: dashboard_user(), Password :: binary()) -> - {ok, Token :: binary()} | {error, Reason :: term()}. + {ok, dashboard_user_role(), Token :: binary()} | {error, Reason :: term()}. sign(User, Password) -> do_sign(User, Password). @@ -120,7 +120,7 @@ do_sign(#?ADMIN{username = Username} = User, Password) -> Role = emqx_dashboard_admin:role(User), JWTRec = format(Token, Username, Role, ExpTime), _ = mria:transaction(?DASHBOARD_SHARD, fun mnesia:write/1, [JWTRec]), - {ok, Token}. + {ok, Role, Token}. -spec do_verify(_, Token :: binary()) -> Result :: diff --git a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl index 141044836..5d2e4f721 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_SUITE.erl @@ -120,6 +120,7 @@ t_rest_api(_Config) -> ?assertEqual( [ filter_req(#{ + <<"backend">> => <<"local">>, <<"username">> => <<"admin">>, <<"description">> => <<"administrator">>, <<"role">> => ?ROLE_SUPERUSER @@ -269,7 +270,7 @@ auth_header_() -> auth_header_(<<"admin">>, <<"public">>). auth_header_(Username, Password) -> - {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), + {ok, _Role, Token} = emqx_dashboard_admin:sign_token(Username, Password), {"Authorization", "Bearer " ++ binary_to_list(Token)}. api_path(Parts) -> @@ -286,6 +287,6 @@ filter_req(Req) -> -else. filter_req(Req) -> - maps:without([role, <<"role">>], Req). + maps:without([role, <<"role">>, backend, <<"backend">>], Req). -endif. diff --git a/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl index c5ee02099..8a6e21fa3 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_admin_SUITE.erl @@ -174,15 +174,16 @@ t_clean_token(_) -> Password = <<"public_www1">>, NewPassword = <<"public_www2">>, {ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, <<"desc">>), - {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), - FakeReq = #{method => <<"GET">>}, + {ok, _, Token} = emqx_dashboard_admin:sign_token(Username, Password), + FakePath = erlang:list_to_binary(emqx_dashboard_swagger:relative_uri("/fake")), + FakeReq = #{method => <<"GET">>, path => FakePath}, {ok, Username} = emqx_dashboard_admin:verify_token(FakeReq, Token), %% change password {ok, _} = emqx_dashboard_admin:change_password(Username, Password, NewPassword), timer:sleep(5), {error, not_found} = emqx_dashboard_admin:verify_token(FakeReq, Token), %% remove user - {ok, Token2} = emqx_dashboard_admin:sign_token(Username, NewPassword), + {ok, _, Token2} = emqx_dashboard_admin:sign_token(Username, NewPassword), {ok, Username} = emqx_dashboard_admin:verify_token(FakeReq, Token2), {ok, _} = emqx_dashboard_admin:remove_user(Username), timer:sleep(5), diff --git a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl index cf022d65d..5a48ef72f 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_api_test_helpers.erl @@ -116,7 +116,7 @@ auth_header(Username) -> auth_header(Username, <<"public">>). auth_header(Username, Password) -> - {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), + {ok, _Role, Token} = emqx_dashboard_admin:sign_token(Username, Password), {"Authorization", "Bearer " ++ binary_to_list(Token)}. multipart_formdata_request(Url, Fields, Files) -> diff --git a/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl index cb6a5a9fd..5323c019e 100644 --- a/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl +++ b/apps/emqx_dashboard/test/emqx_dashboard_haproxy_SUITE.erl @@ -55,7 +55,7 @@ t_status(_Config) -> [binary, {active, false}, {packet, raw}] ), ok = gen_tcp:send(Socket, ranch_proxy_header:header(ProxyInfo)), - {ok, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>), + {ok, _Role, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>), ok = gen_tcp:send( Socket, "GET /status HTTP/1.1\r\n" diff --git a/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl b/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl index 74f6312ea..28bd8960e 100644 --- a/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl +++ b/apps/emqx_dashboard_rbac/src/emqx_dashboard_rbac.erl @@ -12,9 +12,15 @@ %%===================================================================== %% API check_rbac(Req, Extra) -> - Method = cowboy_req:method(Req), Role = role(Extra), - check_rbac_with_method(Role, Method). + Method = cowboy_req:method(Req), + AbsPath = cowboy_req:path(Req), + case emqx_dashboard_swagger:get_relative_uri(AbsPath) of + {ok, Path} -> + check_rbac(Role, Method, Path); + _ -> + false + end. %% For compatibility role(#?ADMIN{role = undefined}) -> @@ -35,11 +41,14 @@ valid_role(Role) -> {error, <<"Role does not exist">>} end. %% =================================================================== -check_rbac_with_method(?ROLE_SUPERUSER, _) -> +check_rbac(?ROLE_SUPERUSER, _, _) -> true; -check_rbac_with_method(?ROLE_VIEWER, <<"GET">>) -> +check_rbac(?ROLE_VIEWER, <<"GET">>, _) -> true; -check_rbac_with_method(_, _) -> +%% this API is a special case +check_rbac(?ROLE_VIEWER, <<"POST">>, <<"/logout">>) -> + true; +check_rbac(_, _, _) -> false. role_list() -> diff --git a/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl b/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl index f5e723a3d..b1a51a3c9 100644 --- a/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl +++ b/apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl @@ -135,8 +135,9 @@ t_clean_token(_) -> Desc = <<"desc">>, NewDesc = <<"new desc">>, {ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, Desc), - {ok, Token} = emqx_dashboard_admin:sign_token(Username, Password), - FakeReq = #{method => <<"GET">>}, + {ok, _Role, Token} = emqx_dashboard_admin:sign_token(Username, Password), + FakePath = erlang:list_to_binary(emqx_dashboard_swagger:relative_uri("/fake")), + FakeReq = #{method => <<"GET">>, path => FakePath}, {ok, Username} = emqx_dashboard_admin:verify_token(FakeReq, Token), %% change description {ok, _} = emqx_dashboard_admin:update_user(Username, ?ROLE_SUPERUSER, NewDesc), @@ -148,6 +149,17 @@ t_clean_token(_) -> {error, not_found} = emqx_dashboard_admin:verify_token(FakeReq, Token), ok. +t_login_out(_) -> + Username = <<"admin_token">>, + Password = <<"public_www1">>, + Desc = <<"desc">>, + {ok, _} = emqx_dashboard_admin:add_user(Username, Password, ?ROLE_SUPERUSER, Desc), + {ok, _Role, Token} = emqx_dashboard_admin:sign_token(Username, Password), + FakePath = erlang:list_to_binary(emqx_dashboard_swagger:relative_uri("/logout")), + FakeReq = #{method => <<"POST">>, path => FakePath}, + {ok, Username} = emqx_dashboard_admin:verify_token(FakeReq, Token), + ok. + add_default_superuser() -> {ok, _NewUser} = emqx_dashboard_admin:add_user( ?DEFAULT_SUPERUSER, diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl index 8fbf220f5..ecc6f40d2 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso.erl @@ -5,6 +5,7 @@ -module(emqx_dashboard_sso). -include_lib("hocon/include/hoconsc.hrl"). +-include_lib("emqx_dashboard/include/emqx_dashboard.hrl"). -export([ hocon_ref/1, @@ -38,7 +39,7 @@ {ok, NewState :: state()} | {error, Reason :: term()}. -callback destroy(State :: state()) -> ok. -callback login(request(), State :: state()) -> - {ok, Token :: binary()} | {error, Reason :: term()}. + {ok, dashboard_user_role(), Token :: binary()} | {error, Reason :: term()}. %%------------------------------------------------------------------------------ %% Callback Interface diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl index 26e2f132f..7bcc65d2e 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_api.erl @@ -83,7 +83,7 @@ schema("/sso/login/:backend") -> parameters => backend_name_in_path(), 'requestBody' => login_union(), responses => #{ - 200 => emqx_dashboard_api:fields([token, version, license]), + 200 => emqx_dashboard_api:fields([role, token, version, license]), 401 => response_schema(401), 404 => response_schema(404) }, @@ -148,10 +148,11 @@ login(post, #{bindings := #{backend := Backend}, body := Sign}) -> State -> Provider = emqx_dashboard_sso:provider(Backend), case emqx_dashboard_sso:login(Provider, Sign, State) of - {ok, Token} -> + {ok, Role, Token} -> ?SLOG(info, #{msg => "dashboard_sso_login_successful", request => Sign}), Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())), {200, #{ + role => Role, token => Token, version => Version, license => #{edition => emqx_release:edition()} diff --git a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl index 395deea7d..d6acbb164 100644 --- a/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl +++ b/apps/emqx_dashboard_sso/src/emqx_dashboard_sso_ldap.erl @@ -106,23 +106,19 @@ ensure_bind_password(Config) -> Config#{bind_password => <<"${password}">>}. adjust_ldap_fields(Fields) -> - adjust_ldap_fields(Fields, []). + lists:map(fun adjust_ldap_field/1, Fields). -adjust_ldap_fields([{filter, Meta} | T], Acc) -> - adjust_ldap_fields( - T, - [ - {filter, Meta#{ - default => <<"(objectClass=user)">>, - example => <<"(objectClass=user)">> - }} - | Acc - ] - ); -adjust_ldap_fields([Any | T], Acc) -> - adjust_ldap_fields(T, [Any | Acc]); -adjust_ldap_fields([], Acc) -> - lists:reverse(Acc). +adjust_ldap_field({base_dn, Meta}) -> + {base_dn, maps:remove(example, Meta)}; +adjust_ldap_field({filter, Meta}) -> + Default = <<"(& (objectClass=person) (uid=${username}))">>, + {filter, Meta#{ + desc => ?DESC(filter), + default => Default, + example => Default + }}; +adjust_ldap_field(Any) -> + Any. login( #{<<"username">> := Username} = Req, diff --git a/apps/emqx_dashboard_sso/test/emqx_dashboard_sso_ldap_SUITE.erl b/apps/emqx_dashboard_sso/test/emqx_dashboard_sso_ldap_SUITE.erl index 605be7fd1..85cb23693 100644 --- a/apps/emqx_dashboard_sso/test/emqx_dashboard_sso_ldap_SUITE.erl +++ b/apps/emqx_dashboard_sso/test/emqx_dashboard_sso_ldap_SUITE.erl @@ -101,7 +101,7 @@ t_first_login(_) -> }, %% this API is authorization-free {ok, 200, Result} = request_without_authorization(post, Path, Req), - ?assertMatch(#{license := _, token := _}, decode_json(Result)), + ?assertMatch(#{license := _, token := _, role := ?ROLE_VIEWER}, decode_json(Result)), ?assertMatch( [#?ADMIN{username = ?SSO_USERNAME(ldap, ?LDAP_USER)}], emqx_dashboard_admin:lookup_user(ldap, ?LDAP_USER) diff --git a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl index 61ed94bdc..dbf034bf8 100644 --- a/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl +++ b/apps/emqx_management/test/emqx_mgmt_api_plugins_SUITE.erl @@ -252,7 +252,7 @@ describe_plugins(Name) -> end. install_plugin(FilePath) -> - {ok, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>), + {ok, _Role, Token} = emqx_dashboard_admin:sign_token(<<"admin">>, <<"public">>), Path = emqx_mgmt_api_test_util:api_path(["plugins", "install"]), case emqx_mgmt_api_test_util:upload_request( diff --git a/rel/i18n/emqx_dashboard_sso_ldap.hocon b/rel/i18n/emqx_dashboard_sso_ldap.hocon index f15975416..db837c81b 100644 --- a/rel/i18n/emqx_dashboard_sso_ldap.hocon +++ b/rel/i18n/emqx_dashboard_sso_ldap.hocon @@ -8,4 +8,11 @@ query_timeout.desc: query_timeout.label: """Query Timeout""" + +filter.desc: +"""The filter for matching users in LDAP is by default `(&(objectClass=person)(uid=${username}))`. For Active Directory, it should be set to `(&(objectClass=user)(sAMAccountName=${username}))` by default. Please refer to [LDAP Filters](https://ldap.com/ldap-filters/) for more details.""" + +filter.label: +"""Filter""" + }