Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard): add SSO feature and integrate with LDAP #11631

Merged
merged 4 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 9 additions & 4 deletions apps/emqx_dashboard/include/emqx_dashboard.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,23 @@
%% a predefined configuration would replace these macros.
-define(ROLE_VIEWER, <<"viewer">>).
-define(ROLE_SUPERUSER, <<"superuser">>).

-define(ROLE_DEFAULT, ?ROLE_SUPERUSER).

-define(SSO_USERNAME(Backend, Name), {Backend, Name}).

-type dashboard_sso_backend() :: atom().
-type dashboard_sso_username() :: {dashboard_sso_backend(), binary()}.
-type dashboard_username() :: binary() | dashboard_sso_username().
-type dashboard_user_role() :: binary().

-record(?ADMIN, {
username :: binary(),
username :: dashboard_username(),
pwdhash :: binary(),
description :: binary(),
role = ?ROLE_DEFAULT :: binary(),
role = ?ROLE_DEFAULT :: dashboard_user_role(),
extra = #{} :: map()
}).

-type dashboard_user_role() :: binary().
-type dashboard_user() :: #?ADMIN{}.

-define(ADMIN_JWT, emqx_admin_jwt).
Expand Down
54 changes: 43 additions & 11 deletions apps/emqx_dashboard/src/emqx_dashboard_admin.erl
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@

-export([backup_tables/0]).

-if(?EMQX_RELEASE_EDITION == ee).
-export([add_sso_user/4, lookup_user/2]).
-endif.

-type emqx_admin() :: #?ADMIN{}.

%%--------------------------------------------------------------------
Expand Down Expand Up @@ -99,10 +103,9 @@ add_default_user() ->
%% API
%%--------------------------------------------------------------------

-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)
->
-spec add_user(dashboard_username(), 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), legal_role(Role)} of
{ok, ok, ok} -> do_add_user(Username, Password, Role, Desc);
{{error, Reason}, _, _} -> {error, Reason};
Expand Down Expand Up @@ -204,7 +207,7 @@ add_user_(Username, Password, Role, Desc) ->
description = Desc
},
mnesia:write(Admin),
#{username => Username, role => Role, description => Desc};
flatten_username(#{username => Username, role => Role, description => Desc});
[_] ->
mnesia:abort(<<"username_already_exist">>)
end.
Expand All @@ -225,7 +228,8 @@ remove_user(Username) when is_binary(Username) ->
{error, Reason}
end.

-spec update_user(binary(), dashboard_user_role(), binary()) -> {ok, map()} | {error, term()}.
-spec update_user(dashboard_username(), dashboard_user_role(), binary()) ->
{ok, map()} | {error, term()}.
update_user(Username, Role, Desc) when is_binary(Username) ->
case legal_role(Role) of
ok ->
Expand Down Expand Up @@ -272,7 +276,10 @@ update_user_(Username, Role, Desc) ->
mnesia:abort(<<"username_not_found">>);
[Admin] ->
mnesia:write(Admin#?ADMIN{role = Role, description = Desc}),
{role(Admin) =:= Role, #{username => Username, role => Role, description => Desc}}
{
role(Admin) =:= Role,
flatten_username(#{username => Username, role => Role, description => Desc})
}
end.

change_password(Username, OldPasswd, NewPasswd) when is_binary(Username) ->
Expand Down Expand Up @@ -312,8 +319,8 @@ update_pwd(Username, Fun) ->
end,
return(mria:transaction(?DASHBOARD_SHARD, Trans)).

-spec lookup_user(binary()) -> [emqx_admin()].
lookup_user(Username) when is_binary(Username) ->
-spec lookup_user(dashboard_username()) -> [emqx_admin()].
lookup_user(Username) ->
Fun = fun() -> mnesia:read(?ADMIN, Username) end,
{atomic, User} = mria:ro_transaction(?DASHBOARD_SHARD, Fun),
User.
Expand All @@ -328,11 +335,11 @@ all_users() ->
role = Role
}
) ->
#{
flatten_username(#{
username => Username,
description => Desc,
role => ensure_role(Role)
}
})
end,
ets:tab2list(?ADMIN)
).
Expand Down Expand Up @@ -410,6 +417,28 @@ legal_role(Role) ->
role(Data) ->
emqx_dashboard_rbac:role(Data).

flatten_username(#{username := ?SSO_USERNAME(Backend, Name)} = Data) ->
Data#{
username := Name,
backend => Backend
};
flatten_username(#{username := Username} = Data) when is_binary(Username) ->
Data#{backend => local}.

-spec add_sso_user(dashboard_sso_backend(), binary(), dashboard_user_role(), binary()) ->
{ok, map()} | {error, any()}.
add_sso_user(Backend, Username0, Role, Desc) when is_binary(Username0) ->
case legal_role(Role) of
ok ->
Username = ?SSO_USERNAME(Backend, Username0),
do_add_user(Username, <<>>, Role, Desc);
{error, _} = Error ->
Error
end.

-spec lookup_user(dashboard_sso_backend(), binary()) -> [emqx_admin()].
lookup_user(Backend, Username) when is_atom(Backend) ->
lookup_user(?SSO_USERNAME(Backend, Username)).
-else.

-dialyzer({no_match, [add_user/4, update_user/3]}).
Expand All @@ -419,6 +448,9 @@ legal_role(_) ->

role(_) ->
?ROLE_DEFAULT.

flatten_username(Data) ->
Data.
-endif.

-ifdef(TEST).
Expand Down
64 changes: 47 additions & 17 deletions apps/emqx_dashboard/src/emqx_dashboard_api.erl
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ schema("/logout") ->
post => #{
tags => [<<"dashboard">>],
desc => ?DESC(logout_api),
parameters => sso_parameters(),
'requestBody' => fields([username]),
responses => #{
204 => <<"Dashboard logout successfully">>,
Expand All @@ -114,7 +115,7 @@ schema("/users") ->
desc => ?DESC(create_user_api),
'requestBody' => fields([username, password, role, description]),
responses => #{
200 => fields([username, role, description])
200 => fields([username, role, description, backend])
}
}
};
Expand All @@ -124,17 +125,17 @@ schema("/users/:username") ->
put => #{
tags => [<<"dashboard">>],
desc => ?DESC(update_user_api),
parameters => fields([username_in_path]),
parameters => sso_parameters(fields([username_in_path])),
'requestBody' => fields([role, description]),
responses => #{
200 => fields([username, role, description]),
200 => fields([username, role, description, backend]),
404 => response_schema(404)
}
},
delete => #{
tags => [<<"dashboard">>],
desc => ?DESC(delete_user_api),
parameters => fields([username_in_path]),
parameters => sso_parameters(fields([username_in_path])),
responses => #{
204 => <<"Delete User successfully">>,
400 => emqx_dashboard_swagger:error_codes(
Expand Down Expand Up @@ -169,7 +170,7 @@ response_schema(404) ->
emqx_dashboard_swagger:error_codes([?USER_NOT_FOUND], ?DESC(users_api404)).

fields(user) ->
fields([username, description]);
fields([username, role, description, backend]);
fields(List) ->
[field(Key) || Key <- List, field_filter(Key)].

Expand Down Expand Up @@ -206,7 +207,10 @@ field(old_pwd) ->
field(new_pwd) ->
{new_pwd, mk(binary(), #{desc => ?DESC(new_pwd)})};
field(role) ->
{role, mk(binary(), #{desc => ?DESC(role), example => ?ROLE_DEFAULT})}.
{role,
mk(binary(), #{desc => ?DESC(role), default => ?ROLE_DEFAULT, example => ?ROLE_DEFAULT})};
field(backend) ->
{backend, mk(binary(), #{desc => ?DESC(backend), example => <<"local">>})}.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enum?


%% -------------------------------------------------------------------------------------------------
%% API
Expand All @@ -229,15 +233,16 @@ login(post, #{body := Params}) ->
end.

logout(_, #{
body := #{<<"username">> := Username},
body := #{<<"username">> := Username0} = Req,
headers := #{<<"authorization">> := <<"Bearer ", Token/binary>>}
}) ->
Username = username(Req, Username0),
case emqx_dashboard_admin:destroy_token_by_username(Username, Token) of
ok ->
?SLOG(info, #{msg => "Dashboard logout successfully", username => Username}),
?SLOG(info, #{msg => "Dashboard logout successfully", username => Username0}),
204;
_R ->
?SLOG(info, #{msg => "Dashboard logout failed.", username => Username}),
?SLOG(info, #{msg => "Dashboard logout failed.", username => Username0}),
{401, ?WRONG_TOKEN_OR_USERNAME, <<"Ensure your token & username">>}
end.

Expand Down Expand Up @@ -266,9 +271,10 @@ users(post, #{body := Params}) ->
end
end.

user(put, #{bindings := #{username := Username}, body := Params}) ->
user(put, #{bindings := #{username := Username0}, body := Params} = Req) ->
Role = maps:get(<<"role">>, Params, ?ROLE_DEFAULT),
Desc = maps:get(<<"description">>, Params),
Username = username(Req, Username0),
case emqx_dashboard_admin:update_user(Username, Role, Desc) of
{ok, Result} ->
{200, filter_result(Result)};
Expand All @@ -277,14 +283,15 @@ user(put, #{bindings := #{username := Username}, body := Params}) ->
{error, Reason} ->
{400, ?BAD_REQUEST, Reason}
end;
user(delete, #{bindings := #{username := Username}, headers := Headers}) ->
case Username == emqx_dashboard_admin:default_username() of
user(delete, #{bindings := #{username := Username0}, headers := Headers} = Req) ->
case Username0 == emqx_dashboard_admin:default_username() of
true ->
?SLOG(info, #{msg => "Dashboard delete admin user failed", username => Username}),
Message = list_to_binary(io_lib:format("Cannot delete user ~p", [Username])),
?SLOG(info, #{msg => "Dashboard delete admin user failed", username => Username0}),
Message = list_to_binary(io_lib:format("Cannot delete user ~p", [Username0])),
{400, ?NOT_ALLOWED, Message};
false ->
case is_self_auth(Username, Headers) of
Username = username(Req, Username0),
case is_self_auth(Username0, Headers) of
true ->
{400, ?NOT_ALLOWED, <<"Cannot delete self">>};
false ->
Expand All @@ -293,13 +300,15 @@ user(delete, #{bindings := #{username := Username}, headers := Headers}) ->
{404, ?USER_NOT_FOUND, Reason};
{ok, _} ->
?SLOG(info, #{
msg => "Dashboard delete admin user", username => Username
msg => "Dashboard delete admin user", username => Username0
}),
{204}
end
end
end.

is_self_auth(?SSO_USERNAME(_, _), _) ->
fasle;
is_self_auth(Username, #{<<"authorization">> := Token}) ->
is_self_auth(Username, Token);
is_self_auth(Username, #{<<"Authorization">> := Token}) ->
Expand Down Expand Up @@ -362,6 +371,19 @@ field_filter(_) ->
filter_result(Result) ->
Result.

sso_parameters() ->
sso_parameters([]).

sso_parameters(Params) ->
emqx_dashboard_sso_api:sso_parameters(Params).

username(#{bindings := #{backend := local}}, Username) ->
Username;
username(#{bindings := #{backend := Backend}}, Username) ->
?SSO_USERNAME(Backend, Username);
username(_Req, Username) ->
Username.

-else.

field_filter(role) ->
Expand All @@ -372,6 +394,14 @@ field_filter(_) ->
filter_result(Result) when is_list(Result) ->
lists:map(fun filter_result/1, Result);
filter_result(Result) ->
maps:without([role], Result).
maps:without([role, backend], Result).

sso_parameters() ->
sso_parameters([]).

sso_parameters(Any) ->
Any.

username(_Req, Username) ->
Username.
-endif.
10 changes: 9 additions & 1 deletion apps/emqx_dashboard/src/emqx_dashboard_token.erl
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ owner(Token) ->
{atomic, []} -> {error, not_found}
end.

jwk(?SSO_USERNAME(Backend, Name), Password, Salt) ->
BackendBin = erlang:atom_to_binary(Backend),
jwk(<<BackendBin/binary, "-", Name/binary>>, Password, Salt);
jwk(Username, Password, Salt) ->
Key = crypto:hash(md5, <<Salt/binary, Username/binary, Password/binary>>),
#{
Expand All @@ -192,12 +195,17 @@ jwt_expiration_time() ->
token_ttl() ->
emqx_conf:get([dashboard, token_expired_time], ?EXPTIME).

format(Token, ?SSO_USERNAME(Backend, Name), Role, ExpTime) ->
format(Token, Backend, Name, Role, ExpTime);
format(Token, Username, Role, ExpTime) ->
format(Token, local, Username, Role, ExpTime).

format(Token, Backend, Username, Role, ExpTime) ->
#?ADMIN_JWT{
token = Token,
username = Username,
exptime = ExpTime,
extra = #{role => Role}
extra = #{role => Role, backend => Backend}
}.

%%--------------------------------------------------------------------
Expand Down
16 changes: 8 additions & 8 deletions apps/emqx_dashboard_rbac/test/emqx_dashboard_rbac_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ t_permission(_) ->
}
),

?assertEqual(
?assertMatch(
#{
<<"username">> => ViewerUser,
<<"role">> => ?ROLE_VIEWER,
<<"description">> => ?ADD_DESCRIPTION
<<"username">> := ViewerUser,
<<"role">> := ?ROLE_VIEWER,
<<"description">> := ?ADD_DESCRIPTION
},
emqx_utils_json:decode(Payload, [return_maps])
),
Expand Down Expand Up @@ -104,11 +104,11 @@ t_update_role(_) ->
}
),

?assertEqual(
?assertMatch(
#{
<<"username">> => ?DEFAULT_SUPERUSER,
<<"role">> => ?ROLE_VIEWER,
<<"description">> => ?ADD_DESCRIPTION
<<"username">> := ?DEFAULT_SUPERUSER,
<<"role">> := ?ROLE_VIEWER,
<<"description">> := ?ADD_DESCRIPTION
},
emqx_utils_json:decode(Payload, [return_maps])
),
Expand Down