Merge pull request #11658 from lafirest/fix/sso_misc

Fix/sso misc
This commit is contained in:
lafirest 2023-09-22 14:44:39 +08:00 committed by GitHub
commit 13b5e4dbc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 82 additions and 46 deletions

View File

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

View File

@ -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">>}

View File

@ -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(<<?BASE_PATH, Path/binary>>) ->
{ok, Path};
get_relative_uri(_Path) ->
error.
file_schema(FileName) ->
#{
content => #{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() ->

View File

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

View File

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

View File

@ -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()}

View File

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

View File

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

View File

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

View File

@ -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"""
}