./ct_report/coverage/mongoose_admin_api.COVER.html

1 -module(mongoose_admin_api).
2
3 -behaviour(mongoose_http_handler).
4
5 %% mongoose_http_handler callbacks
6 -export([config_spec/0, routes/1]).
7
8 %% config processing callbacks
9 -export([process_config/1]).
10
11 %% Utilities for the handler modules
12 -export([init/2,
13 is_authorized/2,
14 parse_body/1,
15 parse_qs/1,
16 try_handle_request/3,
17 throw_error/2,
18 resource_created/4,
19 respond/3]).
20
21 -include("mongoose.hrl").
22 -include("mongoose_config_spec.hrl").
23
24 -type handler_options() :: #{path := string(), username => binary(), password => binary(),
25 atom() => any()}.
26 -type req() :: cowboy_req:req().
27 -type state() :: #{atom() => any()}.
28 -type error_type() :: bad_request | denied | not_found | duplicate | internal.
29
30 -export_type([state/0]).
31
32 -callback routes(state()) -> mongoose_http_handler:routes().
33
34 %% mongoose_http_handler callbacks
35
36 -spec config_spec() -> mongoose_config_spec:config_section().
37 config_spec() ->
38 53 Handlers = all_handlers(),
39 53 #section{items = #{<<"username">> => #option{type = binary},
40 <<"password">> => #option{type = binary},
41 <<"handlers">> => #list{items = #option{type = atom,
42 validate = {enum, Handlers}},
43 validate = unique}},
44 defaults = #{<<"handlers">> => Handlers},
45 process = fun ?MODULE:process_config/1}.
46
47 -spec process_config(handler_options()) -> handler_options().
48 process_config(Opts) ->
49 53 case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of
50 true ->
51 53 Opts;
52 false ->
53
:-(
error(#{what => both_username_and_password_required, opts => Opts})
54 end.
55
56 -spec routes(handler_options()) -> mongoose_http_handler:routes().
57 routes(Opts = #{path := BasePath}) ->
58 60 [{[BasePath, Path], Module, ModuleOpts} || {Path, Module, ModuleOpts} <- api_paths(Opts)].
59
60 all_handlers() ->
61 53 [contacts, users, sessions, messages, stanzas, muc_light, muc, inbox, domain, metrics].
62
63 -spec api_paths(handler_options()) -> mongoose_http_handler:routes().
64 api_paths(#{handlers := Handlers} = Opts) ->
65 60 State = maps:with([username, password], Opts),
66 60 lists:flatmap(fun(Handler) -> api_paths_for_handler(Handler, State) end, Handlers).
67
68 api_paths_for_handler(Handler, State) ->
69 600 HandlerModule = list_to_existing_atom("mongoose_admin_api_" ++ atom_to_list(Handler)),
70 600 HandlerModule:routes(State).
71
72 %% Utilities for the handler modules
73
74 -spec init(req(), state()) -> {cowboy_rest, req(), state()}.
75 init(Req, State) ->
76 862 {cowboy_rest, Req, State}.
77
78 -spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}.
79 is_authorized(Req, State) ->
80 862 AuthDetails = mongoose_api_common:get_auth_details(Req),
81 862 case authorize(State, AuthDetails) of
82 true ->
83 845 {true, Req, State};
84 false ->
85 17 mongoose_api_common:make_unauthorized_response(Req, State)
86 end.
87
88 authorize(#{username := Username, password := Password}, AuthDetails) ->
89 62 case AuthDetails of
90 {AuthMethod, Username, Password} ->
91 51 mongoose_api_common:is_known_auth_method(AuthMethod);
92 _ ->
93 11 false
94 end;
95 authorize(#{}, AuthDetails) ->
96 800 AuthDetails =:= undefined. % Do not accept basic auth when not configured
97
98 -spec parse_body(req()) -> #{atom() => jiffy:json_value()}.
99 parse_body(Req) ->
100 199 try
101 199 {ok, Body, _Req2} = cowboy_req:read_body(Req),
102 199 {DecodedBody} = jiffy:decode(Body),
103 186 maps:from_list([{binary_to_existing_atom(K), V} || {K, V} <- DecodedBody])
104 catch Class:Reason:Stacktrace ->
105 13 ?LOG_WARNING(#{what => parse_body_failed,
106
:-(
class => Class, reason => Reason, stacktrace => Stacktrace}),
107 13 throw_error(bad_request, <<"Invalid request body">>)
108 end.
109
110 -spec parse_qs(req()) -> #{atom() => binary() | true}.
111 parse_qs(Req) ->
112 11 try
113 11 maps:from_list([{binary_to_existing_atom(K), V} || {K, V} <- cowboy_req:parse_qs(Req)])
114 catch Class:Reason:Stacktrace ->
115 1 ?LOG_WARNING(#{what => parse_qs_failed,
116
:-(
class => Class, reason => Reason, stacktrace => Stacktrace}),
117 1 throw_error(bad_request, <<"Invalid query string">>)
118 end.
119
120 -spec try_handle_request(req(), state(), fun((req(), state()) -> Result)) -> Result.
121 try_handle_request(Req, State, F) ->
122 843 try
123 843 F(Req, State)
124 catch throw:#{error_type := ErrorType, message := Msg} ->
125 179 error_response(ErrorType, Msg, Req, State)
126 end.
127
128 -spec throw_error(error_type(), iodata()) -> no_return().
129 throw_error(ErrorType, Msg) ->
130 179 throw(#{error_type => ErrorType, message => Msg}).
131
132 -spec resource_created(req(), state(), iodata(), iodata()) -> {stop, req(), state()}.
133 resource_created(Req, State, Path, Body) ->
134 10 Req2 = cowboy_req:set_resp_body(Body, Req),
135 10 Headers = #{<<"location">> => Path},
136 10 Req3 = cowboy_req:reply(201, Headers, Req2),
137 10 {stop, Req3, State}.
138
139 %% @doc Send response when it can't be returned in a tuple from the handler (e.g. for DELETE)
140 -spec respond(req(), state(), jiffy:json_value()) -> {stop, req(), state()}.
141 respond(Req, State, Response) ->
142 2 Req2 = cowboy_req:set_resp_body(jiffy:encode(Response), Req),
143 2 Req3 = cowboy_req:reply(200, Req2),
144 2 {stop, Req3, State}.
145
146 -spec error_response(error_type(), iodata(), req(), state()) -> {stop, req(), state()}.
147 error_response(ErrorType, Message, Req, State) ->
148 179 BinMessage = iolist_to_binary(Message),
149 179 ?LOG(log_level(ErrorType), #{what => mongoose_admin_api_error_response,
150 error_type => ErrorType,
151 message => BinMessage,
152
:-(
req => Req}),
153 179 Req1 = cowboy_req:reply(error_code(ErrorType), #{}, jiffy:encode(BinMessage), Req),
154 179 {stop, Req1, State}.
155
156 -spec error_code(error_type()) -> non_neg_integer().
157 101 error_code(bad_request) -> 400;
158 30 error_code(denied) -> 403;
159 46 error_code(not_found) -> 404;
160 2 error_code(duplicate) -> 409;
161
:-(
error_code(internal) -> 500.
162
163 -spec log_level(error_type()) -> logger:level().
164 202 log_level(bad_request) -> warning;
165 60 log_level(denied) -> warning;
166 92 log_level(not_found) -> warning;
167 4 log_level(duplicate) -> warning;
168
:-(
log_level(internal) -> error.
Line Hits Source