• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

emqx / emqx / 12410430900

19 Dec 2024 10:02AM UTC coverage: 81.871%. First build
12410430900

Pull #14438

github

web-flow
Merge 3467def95 into 3b5423daa
Pull Request #14438: feat(dashboard): introduced MFA feature and the TOTP method

49 of 235 new or added lines in 11 files covered. (20.85%)

56700 of 69255 relevant lines covered (81.87%)

15268.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

35.19
/apps/emqx_dashboard_mfa/src/emqx_dashboard_mfa_api.erl
1
%%--------------------------------------------------------------------
2
%% Copyright (c) 2024-2025 EMQ Technologies Co., Ltd. All Rights Reserved.
3
%%--------------------------------------------------------------------
4

5
-module(emqx_dashboard_mfa_api).
6

7
-behaviour(minirest_api).
8

9
-include_lib("hocon/include/hoconsc.hrl").
10
-include_lib("emqx/include/logger.hrl").
11

12
-import(hoconsc, [
13
    mk/2,
14
    array/1,
15
    enum/1,
16
    ref/1
17
]).
18

19
-export([
20
    api_spec/0,
21
    paths/0,
22
    schema/1,
23
    namespace/0
24
]).
25

26
-export([
27
    mfa/2,
28
    login/2,
29
    setup_mfa/2
30
]).
31

32
-export([login_union/1, login_resp_union/0, setup_union/0]).
33

34
-define(BAD_REQUEST, 'BAD_REQUEST').
35
-define(USER_NOT_FOUND, 'USER_NOT_FOUND').
36
-define(ERROR_PWD_NOT_MATCH, 'ERROR_PWD_NOT_MATCH').
37
-define(BAD_USERNAME_OR_PWD, 'BAD_USERNAME_OR_PWD').
38

39
-define(TAGS, <<"dashboard">>).
40
-define(MOD_KEY_PATH, [dashboard, mfa]).
41

NEW
42
namespace() -> "dashboard".
×
43

44
api_spec() ->
45
    emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true}).
26✔
46

47
paths() ->
48
    [
26✔
49
        "/login",
50
        "/mfa",
51
        "/users/:username/setup_mfa"
52
    ].
53

54
schema("/login") ->
55
    #{
26✔
56
        'operationId' => login,
57
        post => #{
58
            tags => [<<"dashboard">>],
59
            desc => ?DESC(emqx_dashboard_api, login_api),
60
            summary => <<"Dashboard authentication">>,
61
            'requestBody' => login_union(
62
                hoconsc:ref(emqx_dashboard_api, login)
63
            ),
64
            responses => #{
65
                100 => login_resp_union(),
66
                200 => emqx_dashboard_api:fields([
67
                    role, token, version, license, password_expire_in_seconds
68
                ]),
69
                401 => response_schema(401)
70
            },
71
            security => []
72
        }
73
    };
74
schema("/mfa") ->
75
    #{
26✔
76
        'operationId' => mfa,
77
        get => #{
78
            tags => [?TAGS],
79
            desc => ?DESC(get_mfa),
80
            responses => #{
81
                200 => hoconsc:array(config_union())
82
            }
83
        },
84
        post => #{
85
            tags => [?TAGS],
86
            desc => ?DESC(post_mfa),
87
            'requestBody' => config_union(),
88
            responses => #{
89
                200 => config_union(),
90
                400 => response_schema(400)
91
            }
92
        }
93
    };
94
schema("/users/:username/setup_mfa") ->
95
    #{
26✔
96
        'operationId' => setup_mfa,
97
        post => #{
98
            tags => [<<"dashboard">>],
99
            desc => ?DESC(setup_mfa),
100
            parameters => emqx_dashboard_api:fields([username_in_path]),
101
            'requestBody' => setup_union(),
102
            responses => #{
103
                200 => setup_resp_union(),
104
                404 => response_schema(404),
105
                400 => response_schema(400)
106
            }
107
        }
108
    }.
109

110
config_union() ->
111
    hoconsc:union([
78✔
112
        emqx_dashboard_mfa:api_ref(Mod, api_config)
78✔
113
     || Mod <- emqx_dashboard_mfa:modules()
78✔
114
    ]).
115

116
%% MFA second phase login parameters
117
login_union(Ref) ->
118
    hoconsc:union([
26✔
119
        Ref
120
        | [emqx_dashboard_mfa:api_ref(Mod, second_login) || Mod <- emqx_dashboard_mfa:modules()]
26✔
121
    ]).
122

123
%% MFA first phase login response
124
login_resp_union() ->
125
    hoconsc:union([
26✔
126
        emqx_dashboard_mfa:api_ref(Mod, first_login_resp)
26✔
127
     || Mod <- emqx_dashboard_mfa:modules()
26✔
128
    ]).
129

130
%% MFA setup schema
131
setup_union() ->
132
    hoconsc:union([emqx_dashboard_mfa:api_ref(Mod, setup) || Mod <- emqx_dashboard_mfa:modules()]).
26✔
133

134
setup_resp_union() ->
135
    hoconsc:union([
26✔
136
        emqx_dashboard_mfa:api_ref(Mod, setup_resp)
26✔
137
     || Mod <- emqx_dashboard_mfa:modules()
26✔
138
    ]).
139

140
%%--------------------------------------------------------------------
141
%% API
142
%%--------------------------------------------------------------------
143

144
login(post, #{body := #{<<"username">> := Username, <<"password">> := Password}}) ->
NEW
145
    minirest_handler:update_log_meta(#{log_from => dashboard, log_source => Username}),
×
NEW
146
    case emqx_dashboard_mfa:sign_token(Username, Password) of
×
147
        {ok, Result} ->
NEW
148
            ?SLOG(info, #{msg => "dashboard_login_successful", username => Username}),
×
NEW
149
            Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
×
NEW
150
            {200, Result#{
×
151
                version => Version,
152
                license => #{edition => emqx_release:edition()}
153
            }};
154
        {error, R} ->
NEW
155
            ?SLOG(info, #{msg => "dashboard_login_failed", username => Username, reason => R}),
×
NEW
156
            {401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>}
×
157
    end;
158
login(post, #{body := Params}) ->
NEW
159
    case emqx_dashboard_mfa:verify(Params) of
×
160
        {ok, Result} ->
NEW
161
            ?SLOG(info, #{msg => "dashboard_mfa_login_successful"}),
×
NEW
162
            Version = iolist_to_binary(proplists:get_value(version, emqx_sys:info())),
×
NEW
163
            {200, Result#{
×
164
                version => Version,
165
                license => #{edition => emqx_release:edition()}
166
            }};
167
        {error, Reason} ->
NEW
168
            ?SLOG(info, #{msg => "dashboard_mfa_login_failed", reason => Reason}),
×
NEW
169
            {401, ?BAD_USERNAME_OR_PWD, <<"Auth failed">>}
×
170
    end.
171

172
mfa(get, _Request) ->
NEW
173
    Methods = emqx:get_config(?MOD_KEY_PATH, #{}),
×
NEW
174
    {200,
×
175
        lists:map(
176
            fun({Method, Config}) ->
NEW
177
                Config#{method => Method}
×
178
            end,
179
            maps:to_list(Methods)
180
        )};
181
mfa(post, #{body := #{<<"method">> := Method} = Params}) ->
NEW
182
    {ok, Method2} = emqx_utils:safe_to_existing_atom(Method),
×
NEW
183
    Config = emqx_utils_maps:safe_atom_key_map(maps:without([<<"method">>], Params)),
×
NEW
184
    case emqx_conf:update([dashboard, mfa, Method2], Config, #{override_to => cluster}) of
×
185
        {ok, _Result} ->
NEW
186
            {200, Params};
×
187
        {error, Reason} ->
NEW
188
            {400, #{code => ?BAD_REQUEST, message => Reason}}
×
189
    end.
190

191
setup_mfa(
192
    post,
193
    #{
194
        bindings := #{username := Username},
195
        body := #{<<"password">> := Password} = Params
196
    } = _Req
197
) ->
NEW
198
    LogMeta = #{msg => "dashboard_setup_mfa", username => binary_to_list(Username)},
×
NEW
199
    case emqx_dashboard_admin:check(Username, Password) of
×
200
        {ok, _User} ->
NEW
201
            case emqx_dashboard_mfa:setup_user_mfa(Username, Params) of
×
202
                {ok, Result} ->
NEW
203
                    ?SLOG(info, LogMeta#{result => success}),
×
NEW
204
                    {200, Result};
×
205
                {error, Reason} ->
NEW
206
                    ?SLOG(error, LogMeta#{result => failed, reason => Reason}),
×
NEW
207
                    {400, ?BAD_REQUEST, Reason}
×
208
            end;
209
        {error, <<"username_not_found">>} ->
NEW
210
            ?SLOG(error, LogMeta#{result => failed, reason => "username not found"}),
×
NEW
211
            {404, ?USER_NOT_FOUND, <<"User not found">>};
×
212
        {error, <<"password_error">>} ->
NEW
213
            ?SLOG(error, LogMeta#{result => failed, reason => "wrong password"}),
×
NEW
214
            {400, ?ERROR_PWD_NOT_MATCH, <<"Wrong password">>};
×
215
        {error, Reason} ->
NEW
216
            ?SLOG(error, LogMeta#{result => failed, reason => Reason}),
×
NEW
217
            {400, ?BAD_REQUEST, Reason}
×
218
    end.
219

220
%%--------------------------------------------------------------------
221
%% internal
222
%%--------------------------------------------------------------------
223
response_schema(400) ->
224
    emqx_dashboard_swagger:error_codes([?BAD_REQUEST], ?DESC(bad_request_response400));
52✔
225
response_schema(Other) ->
226
    emqx_dashboard_api:response_schema(Other).
52✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc