Skip to content

Commit

Permalink
Merge pull request #11631 from lafirest/feat/dashboard_ldap
Browse files Browse the repository at this point in the history
feat(dashboard): add SSO feature and integrate with LDAP
  • Loading branch information
lafirest committed Sep 20, 2023
2 parents 6581874 + 6131108 commit bc6edac
Show file tree
Hide file tree
Showing 27 changed files with 1,162 additions and 47 deletions.
13 changes: 9 additions & 4 deletions apps/emqx_dashboard/include/emqx_dashboard.hrl
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
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
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">>})}.

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

0 comments on commit bc6edac

Please sign in to comment.