./ct_report/coverage/mongoose_api_admin.COVER.html

1 %%%-------------------------------------------------------------------
2 %%% @author ludwikbukowski
3 %%% @copyright (C) 2016, Erlang Solutions Ltd.
4 %%% Created : 05. Jul 2016 12:59
5 %%%-------------------------------------------------------------------
6
7 %% @doc MongooseIM REST HTTP API for administration.
8 %% This module implements cowboy REST callbacks and
9 %% passes the requests on to the http api backend module.
10 %% @end
11 -module(mongoose_api_admin).
12 -author("ludwikbukowski").
13
14 -behaviour(mongoose_http_handler).
15 -behaviour(cowboy_rest).
16
17 %% mongoose_http_handler callbacks
18 -export([config_spec/0, routes/1]).
19
20 %% config processing callbacks
21 -export([process_config/1]).
22
23 %% cowboy_rest exports
24 -export([allowed_methods/2,
25 content_types_provided/2,
26 terminate/3,
27 init/2,
28 options/2,
29 content_types_accepted/2,
30 delete_resource/2,
31 is_authorized/2]).
32 %% local callbacks
33 -export([to_json/2, from_json/2]).
34
35 -ignore_xref([cowboy_router_paths/2, from_json/2, to_json/2]).
36
37 -include("mongoose_api.hrl").
38 -include("mongoose.hrl").
39 -include("mongoose_config_spec.hrl").
40
41 -import(mongoose_api_common, [error_response/4,
42 action_to_method/1,
43 method_to_action/1,
44 error_code/1,
45 process_request/4,
46 parse_request_body/1]).
47
48 -type credentials() :: {Username :: binary(), Password :: binary()} | any.
49
50 -type handler_options() :: #{path := string(), username => binary(), password => binary(),
51 atom() => any()}.
52
53 %%--------------------------------------------------------------------
54 %% mongoose_http_handler callbacks
55 %%--------------------------------------------------------------------
56
57 -spec config_spec() -> mongoose_config_spec:config_section().
58 config_spec() ->
59 83 #section{items = #{<<"username">> => #option{type = binary},
60 <<"password">> => #option{type = binary}},
61 process = fun ?MODULE:process_config/1}.
62
63 -spec process_config(handler_options()) -> handler_options().
64 process_config(Opts) ->
65 83 case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of
66 true ->
67 83 Opts;
68 false ->
69
:-(
error(#{what => both_username_and_password_required, opts => Opts})
70 end.
71
72 -spec routes(handler_options()) -> mongoose_http_handler:routes().
73 routes(Opts = #{path := BasePath}) ->
74 159 ejabberd_hooks:add(register_command, global, mongoose_api_common, reload_dispatches, 50),
75 159 ejabberd_hooks:add(unregister_command, global, mongoose_api_common, reload_dispatches, 50),
76 159 try
77 159 Commands = mongoose_commands:list(admin),
78 159 [handler_path(BasePath, Command, Opts) || Command <- Commands]
79 catch
80 Class:Err:StackTrace ->
81
:-(
?LOG_ERROR(#{what => getting_command_list_error,
82
:-(
class => Class, reason => Err, stacktrace => StackTrace}),
83
:-(
[]
84 end.
85
86 %%--------------------------------------------------------------------
87 %% cowboy_rest callbacks
88 %%--------------------------------------------------------------------
89
90 init(Req, Opts) ->
91 101 Bindings = maps:to_list(cowboy_req:bindings(Req)),
92 101 #{command_category := CommandCategory, command_subcategory := CommandSubCategory} = Opts,
93 101 Auth = auth_opts(Opts),
94 101 State = #http_api_state{allowed_methods = mongoose_api_common:get_allowed_methods(admin),
95 bindings = Bindings,
96 command_category = CommandCategory,
97 command_subcategory = CommandSubCategory,
98 auth = Auth},
99 101 {cowboy_rest, Req, State}.
100
101 2 auth_opts(#{username := UserName, password := Password}) -> {UserName, Password};
102 99 auth_opts(#{}) -> any.
103
104 options(Req, State) ->
105
:-(
Req1 = set_cors_headers(Req),
106
:-(
{ok, Req1, State}.
107
108 set_cors_headers(Req) ->
109
:-(
Req1 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Methods">>,
110 <<"GET, OPTIONS, PUT, POST, DELETE">>, Req),
111
:-(
Req2 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Origin">>,
112 <<"*">>, Req1),
113
:-(
cowboy_req:set_resp_header(<<"Access-Control-Allow-Headers">>,
114 <<"Content-Type">>, Req2).
115
116 allowed_methods(Req, #http_api_state{command_category = Name} = State) ->
117 101 CommandList = mongoose_commands:list(admin, Name),
118 101 AllowedMethods = [action_to_method(mongoose_commands:action(Command))
119 101 || Command <- CommandList],
120 101 {[<<"OPTIONS">> | AllowedMethods], Req, State}.
121
122 content_types_provided(Req, State) ->
123 100 CTP = [{{<<"application">>, <<"json">>, '*'}, to_json}],
124 100 {CTP, Req, State}.
125
126 content_types_accepted(Req, State) ->
127 58 CTA = [{{<<"application">>, <<"json">>, '*'}, from_json}],
128 58 {CTA, Req, State}.
129
130 terminate(_Reason, _Req, _State) ->
131 101 ok.
132
133 %% @doc Called for a method of type "DELETE"
134 delete_resource(Req, #http_api_state{command_category = Category,
135 command_subcategory = SubCategory,
136 bindings = B} = State) ->
137 9 Arity = length(B),
138 9 Cmds = mongoose_commands:list(admin, Category, method_to_action(<<"DELETE">>), SubCategory),
139 9 [Command] = [C || C <- Cmds, mongoose_commands:arity(C) == Arity],
140 9 process_request(<<"DELETE">>, Command, Req, State).
141
142
143 %%--------------------------------------------------------------------
144 %% Authorization
145 %%--------------------------------------------------------------------
146
147 % @doc Cowboy callback
148 is_authorized(Req, State) ->
149 101 ControlCreds = get_control_creds(State),
150 101 AuthDetails = mongoose_api_common:get_auth_details(Req),
151 101 case authorize(ControlCreds, AuthDetails) of
152 true ->
153 100 {true, Req, State};
154 false ->
155 1 mongoose_api_common:make_unauthorized_response(Req, State)
156 end.
157
158 -spec authorize(credentials(), {AuthMethod :: atom(),
159 Username :: binary(),
160 Password :: binary()}) -> boolean().
161 99 authorize(any, _) -> true;
162
:-(
authorize(_, undefined) -> false;
163 authorize(ControlCreds, {AuthMethod, User, Password}) ->
164 2 compare_creds(ControlCreds, {User, Password}) andalso
165 1 mongoose_api_common:is_known_auth_method(AuthMethod).
166
167 % @doc Checks if credentials are the same (if control creds are 'any'
168 % it is equal to everything).
169 -spec compare_creds(credentials(), credentials() | undefined) -> boolean().
170 1 compare_creds({User, Pass}, {User, Pass}) -> true;
171 1 compare_creds(_, _) -> false.
172
173 get_control_creds(#http_api_state{auth = Creds}) ->
174 101 Creds.
175
176 %%--------------------------------------------------------------------
177 %% Internal funs
178 %%--------------------------------------------------------------------
179
180 %% @doc Called for a method of type "GET"
181 to_json(Req, #http_api_state{command_category = Category,
182 command_subcategory = SubCategory,
183 bindings = B} = State) ->
184 33 Cmds = mongoose_commands:list(admin, Category, method_to_action(<<"GET">>), SubCategory),
185 33 Arity = length(B),
186 33 case [C || C <- Cmds, mongoose_commands:arity(C) == Arity] of
187 [Command] ->
188 32 process_request(<<"GET">>, Command, Req, State);
189 [] ->
190 1 error_response(not_found, ?ARGS_LEN_ERROR, Req, State)
191 end.
192
193 %% @doc Called for a method of type "POST" and "PUT"
194 from_json(Req, #http_api_state{command_category = Category,
195 command_subcategory = SubCategory,
196 bindings = B} = State) ->
197 58 case parse_request_body(Req) of
198 {error, _R}->
199
:-(
error_response(bad_request, ?BODY_MALFORMED, Req, State);
200 {Params, _} ->
201 58 Method = cowboy_req:method(Req),
202 58 Cmds = mongoose_commands:list(admin, Category, method_to_action(Method), SubCategory),
203 58 QVals = cowboy_req:parse_qs(Req),
204 58 Arity = length(B) + length(Params) + length(QVals),
205 58 case [C || C <- Cmds, mongoose_commands:arity(C) == Arity] of
206 [Command] ->
207 58 process_request(Method, Command, {Params, Req}, State);
208 [] ->
209
:-(
error_response(not_found, ?ARGS_LEN_ERROR, Req, State)
210 end
211 end.
212
213 -spec handler_path(ejabberd_cowboy:path(), mongoose_commands:t(), handler_options()) ->
214 ejabberd_cowboy:route().
215 handler_path(Base, Command, CommonOpts) ->
216 3719 {[Base, mongoose_api_common:create_admin_url_path(Command)],
217 ?MODULE, CommonOpts#{command_category => mongoose_commands:category(Command),
218 command_subcategory => mongoose_commands:subcategory(Command)}}.
Line Hits Source