1 |
|
%% @doc Mongoose version of command management |
2 |
|
%% The following is loosely based on old ejabberd_commands implementation, |
3 |
|
%% with some modification related to type check, permission control |
4 |
|
%% and the likes. |
5 |
|
%% |
6 |
|
%% This is a central registry of commands which can be exposed via |
7 |
|
%% REST, XMPP as ad-hoc commands or in any other way. Any module can |
8 |
|
%% define its commands and register them here. |
9 |
|
%% |
10 |
|
%% ==== Usage ==== |
11 |
|
%% |
12 |
|
%% A module defines a list of commands; a command definition is a proplist |
13 |
|
%% with the following keys: |
14 |
|
%% name :: atom() |
15 |
|
%% name of the command by which we refer to it |
16 |
|
%% category :: binary() |
17 |
|
%% this defines what group the command belongs to, like user, chatroom etc |
18 |
|
%% desc :: binary() |
19 |
|
%% long description |
20 |
|
%% module :: module() |
21 |
|
%% module to call |
22 |
|
%% function :: atom() |
23 |
|
%% function to call |
24 |
|
%% action :: command_action() |
25 |
|
%% so that the HTTP side can decide which verb to require |
26 |
|
%% args = [] :: [argspec()] |
27 |
|
%% Type spec - see below; this is both for introspection and type check on call. Args spec is more |
28 |
|
%% limited then return, it has to be a list of named arguments, like [{id, integer}, {msg, binary}] |
29 |
|
%% security_policy = [atom()] (optional) |
30 |
|
%% permissions required to run this command, defaults to [admin] |
31 |
|
%% result :: argspec() |
32 |
|
%% Type spec of return value of the function to call; execute/3 eventually returns {ok, result} |
33 |
|
%% identifiers :: [atom()] (optional, required in 'update' commands) |
34 |
|
%% optargs :: [{atom(), type(), term()] (optional args with type and default value. |
35 |
|
%% Then a command is called, it fills missing arguments with values from here. |
36 |
|
%% We have then two arities: arity of a command, which is only its required arguments, |
37 |
|
%% and arity of the function to be called, which is required args + optional args. |
38 |
|
%% |
39 |
|
%% You can ignore return value of the target func by specifying return value as {result, ok}. The |
40 |
|
%% execute/3 will then always return just 'ok' (or error). |
41 |
|
%% |
42 |
|
%% If action is 'update' then it MUST specify which args are to be used as identifiers of object |
43 |
|
%% to update. It has no effect on how the engine does its job, but may be used by client code |
44 |
|
%% to enforce proper structure of request. (this is bad programming practice but we didn't have |
45 |
|
%% a better idea, we had to solve it for REST API) |
46 |
|
%% |
47 |
|
%% Commands are registered here upon the module's initialisation |
48 |
|
%% (the module has to explicitly call mongoose_commands:register_commands/1 |
49 |
|
%% func, it doesn't happen automagically), also should be unregistered when module |
50 |
|
%% terminates. |
51 |
|
%% |
52 |
|
%% Commands are executed by calling mongoose_commands:execute/3 method. This |
53 |
|
%% can return: |
54 |
|
%% {ok, Result} |
55 |
|
%% {error, denied, Msg} if user has no permission |
56 |
|
%% {error, not_implemented, Msg} |
57 |
|
%% {error, type_error, Msg} if either arguments or return value does not match |
58 |
|
%% {error, internal, Msg} if an exception was caught |
59 |
|
%% |
60 |
|
%% ==== Type check ==== |
61 |
|
%% |
62 |
|
%% A command's definition includes specification of it arguments; when |
63 |
|
%% it is called, arguments are check for compatibility. Examples of specs |
64 |
|
%% and compliant arguments: |
65 |
|
%% |
66 |
|
%% ``` |
67 |
|
%% a single type spec |
68 |
|
%% integer 2 |
69 |
|
%% binary <<"zzz">> |
70 |
|
%% atom brrr |
71 |
|
%% a list of arbitrary length, of a given type |
72 |
|
%% [integer] [] |
73 |
|
%% [integer] [1] |
74 |
|
%% [integer] [1, 2, 3, 4] |
75 |
|
%% a list of anything |
76 |
|
%% [] |
77 |
|
%% a named argument (name is only for clarity) |
78 |
|
%% {msg, binary} <<"zzz">> |
79 |
|
%% a tuple of args |
80 |
|
%% {integer, binary, float} {1, <<"2">>, 3.0} |
81 |
|
%% a tuple of named args |
82 |
|
%% {{x, integer}, {y, binary}} {1, <<"bbb">>} |
83 |
|
%% ''' |
84 |
|
%% |
85 |
|
%% Arg specification is used at call-time for control, and also for introspection |
86 |
|
%% (see list/1, list/2, mongoose_commands:get_command/2 and args/1) |
87 |
|
%% |
88 |
|
%% Return value is also specified, and this is a bit tricky: command definition |
89 |
|
%% contains spec of return value - what the target func returns should comply to it. |
90 |
|
%% The registry, namely execute/3, returns a tuple {ok, ValueReturnedByTheFunction} |
91 |
|
%% If return value is defined as 'ok' then whatever target func returns is ignored. |
92 |
|
%% This is mostly to make a distinction between 'create' actions which actually create something |
93 |
|
%% and return its identifier and those 'lame creators' which cause some action to be done and |
94 |
|
%% something written to dbase (exemplum: sending a message), but there is no accessible resource. |
95 |
|
%% |
96 |
|
%% Called function may also return a tuple {error, term()}, this is returned by the registry |
97 |
|
%% as {error, internal, Msg::binary()} |
98 |
|
%% |
99 |
|
%% ==== Permission control ==== |
100 |
|
%% |
101 |
|
%% First argument to every function exported from this module is always |
102 |
|
%% a user. If you call it from trusted place, you can pass 'admin' here and |
103 |
|
%% the whole permission check is skipped. Otherwise, pass #jid record. |
104 |
|
%% |
105 |
|
%% A command MAY define a security policy to be applied |
106 |
|
%% (and this is not yet designed) |
107 |
|
%% If it doesn't, then the command is accessible to 'admin' calls only. |
108 |
|
%% |
109 |
|
|
110 |
|
-module(mongoose_commands). |
111 |
|
-author("bartlomiej.gorny@erlang-solutions.com"). |
112 |
|
-include("mongoose.hrl"). |
113 |
|
-include("jlib.hrl"). |
114 |
|
|
115 |
|
-record(mongoose_command, |
116 |
|
{ |
117 |
|
%% name of the command by which we refer to it |
118 |
|
name :: atom(), |
119 |
|
%% groups commands related to the same functionality (user managment, messages/archive) |
120 |
|
category :: binary(), |
121 |
|
%% optimal subcategory |
122 |
|
subcategory = undefined :: undefined | binary(), |
123 |
|
%% long description |
124 |
|
desc :: binary(), |
125 |
|
%% module to call |
126 |
|
module :: module(), |
127 |
|
%% function to call |
128 |
|
function :: atom(), |
129 |
|
%% so that the HTTP side can decide which verb to require |
130 |
|
action :: action(), |
131 |
|
%% this is both for introspection and type check on call |
132 |
|
args = [] :: [argspec()], |
133 |
|
%% arg which has a default value and is optional |
134 |
|
optargs = [] :: [optargspec()], |
135 |
|
%% internal use |
136 |
|
caller_pos :: integer(), |
137 |
|
%% resource identifiers, a subset of args |
138 |
|
identifiers = [] :: [atom()], |
139 |
|
%% permissions required to run this command |
140 |
|
security_policy = [admin] :: security(), |
141 |
|
%% what the called func should return; if ok then return of called function is ignored |
142 |
|
result :: argspec()|ok |
143 |
|
}). |
144 |
|
|
145 |
|
-opaque t() :: #mongoose_command{}. |
146 |
|
-type caller() :: admin | binary() | user. |
147 |
|
-type action() :: create | read | update | delete. %% just basic CRUD; sending a mesage is 'create' |
148 |
|
|
149 |
|
-type typedef() :: [typedef_basic()] | typedef_basic(). |
150 |
|
|
151 |
|
-type typedef_basic() :: boolean | integer | binary | float. %% most basic primitives, string is a binary |
152 |
|
|
153 |
|
-type argspec() :: typedef() |
154 |
|
| {atom(), typedef()} %% a named argument |
155 |
|
| {argspec()} % a tuple of a few args (can be of any size) |
156 |
|
| [typedef()]. % a list, but one element |
157 |
|
|
158 |
|
-type optargspec() :: {atom(), typedef(), term()}. % name, type, default value |
159 |
|
|
160 |
|
-type security() :: [admin | user]. %% later acl option will be added |
161 |
|
|
162 |
|
-type errortype() :: denied | not_implemented | bad_request | type_error | not_found | internal. |
163 |
|
-type errorreason() :: term(). |
164 |
|
|
165 |
|
-type args() :: [{atom(), term()}] | map(). |
166 |
|
-type failure() :: {error, errortype(), errorreason()}. |
167 |
|
-type success() :: ok | {ok, term()}. |
168 |
|
|
169 |
|
-export_type([t/0]). |
170 |
|
-export_type([caller/0]). |
171 |
|
-export_type([action/0]). |
172 |
|
-export_type([argspec/0]). |
173 |
|
-export_type([optargspec/0]). |
174 |
|
-export_type([errortype/0]). |
175 |
|
-export_type([errorreason/0]). |
176 |
|
-export_type([failure/0]). |
177 |
|
|
178 |
|
-type command_properties() :: [{atom(), term()}]. |
179 |
|
|
180 |
|
%%%% API |
181 |
|
|
182 |
|
-export([check_type/3]). |
183 |
|
-export([init/0]). |
184 |
|
|
185 |
|
-export([register/1, |
186 |
|
unregister/1, |
187 |
|
list/1, |
188 |
|
list/2, |
189 |
|
list/3, |
190 |
|
list/4, |
191 |
|
register_commands/1, |
192 |
|
unregister_commands/1, |
193 |
|
new/1, |
194 |
|
get_command/2, |
195 |
|
execute/3, |
196 |
|
name/1, |
197 |
|
category/1, |
198 |
|
subcategory/1, |
199 |
|
desc/1, |
200 |
|
args/1, |
201 |
|
optargs/1, |
202 |
|
arity/1, |
203 |
|
func_arity/1, |
204 |
|
identifiers/1, |
205 |
|
action/1, |
206 |
|
result/1 |
207 |
|
]). |
208 |
|
|
209 |
|
-ignore_xref([check_type/3, func_arity/1, get_command/2, list/3, new/1, |
210 |
|
register_commands/1, unregister_commands/1, result/1]). |
211 |
|
|
212 |
|
%% @doc creates new command object based on provided proplist |
213 |
|
-spec new(command_properties()) -> t(). |
214 |
|
new(Props) -> |
215 |
16670 |
Fields = record_info(fields, mongoose_command), |
216 |
16670 |
Lst = check_command([], Props, Fields), |
217 |
16670 |
RLst = lists:reverse(Lst), |
218 |
16670 |
Cmd = list_to_tuple([mongoose_command|RLst]), |
219 |
16670 |
check_identifiers(Cmd#mongoose_command.action, |
220 |
|
Cmd#mongoose_command.identifiers, |
221 |
|
Cmd#mongoose_command.args), |
222 |
|
% store position of caller in args (if present) |
223 |
16670 |
Cmd#mongoose_command{caller_pos = locate_caller(Cmd#mongoose_command.args)}. |
224 |
|
|
225 |
|
|
226 |
|
%% @doc Register mongoose commands. This can be run by any module that wants its commands exposed. |
227 |
|
-spec register([command_properties()]) -> ok. |
228 |
|
register(Cmds) -> |
229 |
926 |
Commands = [new(C) || C <- Cmds], |
230 |
926 |
register_commands(Commands). |
231 |
|
|
232 |
|
%% @doc Unregister mongoose commands. Should be run when module is unloaded. |
233 |
|
-spec unregister([command_properties()]) -> ok. |
234 |
|
unregister(Cmds) -> |
235 |
926 |
Commands = [new(C) || C <- Cmds], |
236 |
926 |
unregister_commands(Commands). |
237 |
|
|
238 |
|
%% @doc List commands, available for this user. |
239 |
|
-spec list(caller()) -> [t()]. |
240 |
|
list(U) -> |
241 |
231 |
list(U, any, any, any). |
242 |
|
|
243 |
|
%% @doc List commands, available for this user, filtered by category. |
244 |
|
-spec list(caller(), binary() | any) -> [t()]. |
245 |
|
list(U, C) -> |
246 |
2282 |
list(U, C, any, any). |
247 |
|
|
248 |
|
%% @doc List commands, available for this user, filtered by category and action. |
249 |
|
-spec list(caller(), binary() | any, atom()) -> [t()]. |
250 |
|
list(U, Category, Action) -> |
251 |
:-( |
list(U, Category, Action, any). |
252 |
|
|
253 |
|
%% @doc List commands, available for this user, filtered by category, action and subcategory |
254 |
|
-spec list(caller(), binary() | any, atom(), binary() | any | undefined) -> [t()]. |
255 |
|
list(U, Category, Action, SubCategory) -> |
256 |
2615 |
CL = command_list(Category, Action, SubCategory), |
257 |
2615 |
lists:filter(fun(C) -> is_available_for(U, C) end, CL). |
258 |
|
|
259 |
|
%% @doc Get command definition, if allowed for this user. |
260 |
|
-spec get_command(caller(), atom()) -> t(). |
261 |
|
get_command(Caller, Name) -> |
262 |
:-( |
case ets:lookup(mongoose_commands, Name) of |
263 |
|
[C] -> |
264 |
:-( |
case is_available_for(Caller, C) of |
265 |
|
true -> |
266 |
:-( |
C; |
267 |
|
false -> |
268 |
:-( |
{error, denied, <<"Command not available">>} |
269 |
|
end; |
270 |
:-( |
[] -> {error, not_implemented, <<"Command not implemented">>} |
271 |
|
end. |
272 |
|
|
273 |
|
%% accessors |
274 |
|
-spec name(t()) -> atom(). |
275 |
|
name(Cmd) -> |
276 |
8598 |
Cmd#mongoose_command.name. |
277 |
|
|
278 |
|
-spec category(t()) -> binary(). |
279 |
|
category(Cmd) -> |
280 |
14679 |
Cmd#mongoose_command.category. |
281 |
|
|
282 |
|
-spec subcategory(t()) -> binary() | undefined. |
283 |
|
subcategory(Cmd) -> |
284 |
15319 |
Cmd#mongoose_command.subcategory. |
285 |
|
|
286 |
|
-spec desc(t()) -> binary(). |
287 |
|
desc(Cmd) -> |
288 |
163 |
Cmd#mongoose_command.desc. |
289 |
|
|
290 |
|
-spec args(t()) -> term(). |
291 |
|
args(Cmd) -> |
292 |
3436 |
Cmd#mongoose_command.args. |
293 |
|
|
294 |
|
-spec optargs(t()) -> term(). |
295 |
|
optargs(Cmd) -> |
296 |
101 |
Cmd#mongoose_command.optargs. |
297 |
|
|
298 |
|
-spec identifiers(t()) -> [atom()]. |
299 |
|
identifiers(Cmd) -> |
300 |
1806 |
Cmd#mongoose_command.identifiers. |
301 |
|
|
302 |
|
-spec action(t()) -> action(). |
303 |
|
action(Cmd) -> |
304 |
18832 |
Cmd#mongoose_command.action. |
305 |
|
|
306 |
|
-spec result(t()) -> term(). |
307 |
|
result(Cmd) -> |
308 |
:-( |
Cmd#mongoose_command.result. |
309 |
|
|
310 |
|
-spec arity(t()) -> integer(). |
311 |
|
arity(Cmd) -> |
312 |
8518 |
length(Cmd#mongoose_command.args). |
313 |
|
|
314 |
|
-spec func_arity(t()) -> integer(). |
315 |
|
func_arity(Cmd) -> |
316 |
:-( |
length(Cmd#mongoose_command.args) + length(Cmd#mongoose_command.optargs). |
317 |
|
|
318 |
|
%% @doc Command execution. |
319 |
|
-spec execute(caller(), atom() | t(), args()) -> |
320 |
|
success() | failure(). |
321 |
|
execute(Caller, Name, Args) when is_atom(Name) -> |
322 |
125 |
case ets:lookup(mongoose_commands, Name) of |
323 |
125 |
[Command] -> execute_command(Caller, Command, Args); |
324 |
:-( |
[] -> {error, not_implemented, {command_not_supported, Name, sizeof(Args)}} |
325 |
|
end; |
326 |
|
execute(Caller, #mongoose_command{name = Name}, Args) -> |
327 |
:-( |
execute(Caller, Name, Args). |
328 |
|
|
329 |
|
init() -> |
330 |
80 |
ets:new(mongoose_commands, [named_table, set, public, |
331 |
|
{keypos, #mongoose_command.name}]). |
332 |
|
|
333 |
|
%%%% end of API |
334 |
|
-spec register_commands([t()]) -> ok. |
335 |
|
register_commands(Commands) -> |
336 |
926 |
lists:foreach( |
337 |
|
fun(Command) -> |
338 |
8335 |
check_registration(Command), %% may throw |
339 |
8335 |
ets:insert_new(mongoose_commands, Command), |
340 |
8335 |
mongoose_hooks:register_command(Command), |
341 |
8335 |
ok |
342 |
|
end, |
343 |
|
Commands). |
344 |
|
|
345 |
|
-spec unregister_commands([t()]) -> ok. |
346 |
|
unregister_commands(Commands) -> |
347 |
926 |
lists:foreach( |
348 |
|
fun(Command) -> |
349 |
8335 |
ets:delete_object(mongoose_commands, Command), |
350 |
8335 |
mongoose_hooks:unregister_command(Command) |
351 |
|
end, |
352 |
|
Commands). |
353 |
|
|
354 |
|
-spec execute_command(caller(), atom() | t(), args()) -> |
355 |
|
success() | failure(). |
356 |
|
execute_command(Caller, Command, Args) -> |
357 |
125 |
try check_and_execute(Caller, Command, Args) of |
358 |
|
ignore_result -> |
359 |
38 |
ok; |
360 |
|
{error, Type, Reason} -> |
361 |
30 |
{error, Type, Reason}; |
362 |
|
{ok, Res} -> |
363 |
56 |
{ok, Res} |
364 |
|
catch |
365 |
|
% admittedly, not the best style of coding, in Erlang at least. But we have to do plenty |
366 |
|
% of various checks, and in absence of something like Elixir's "with" construct we are |
367 |
|
% facing a choice between throwing stuff or using some more or less tortured syntax |
368 |
|
% to chain these checks. |
369 |
|
throw:{Type, Reason} -> |
370 |
1 |
{error, Type, Reason}; |
371 |
|
Class:Reason:Stacktrace -> |
372 |
:-( |
Err = #{what => command_failed, |
373 |
|
command_name => Command#mongoose_command.name, |
374 |
|
caller => Caller, args => Args, |
375 |
|
class => Class, reason => Reason, stacktrace => Stacktrace}, |
376 |
:-( |
?LOG_ERROR(Err), |
377 |
:-( |
{error, internal, mongoose_lib:term_to_readable_binary(Err)} |
378 |
|
end. |
379 |
|
|
380 |
|
add_defaults(Args, Opts) when is_map(Args) -> |
381 |
125 |
COpts = [{K, V} || {K, _, V} <- Opts], |
382 |
125 |
Missing = lists:subtract(proplists:get_keys(Opts), maps:keys(Args)), |
383 |
125 |
lists:foldl(fun(K, Ar) -> maps:put(K, proplists:get_value(K, COpts), Ar) end, |
384 |
|
Args, Missing). |
385 |
|
|
386 |
|
% @doc This performs many checks - types, permissions etc, may throw one of many exceptions |
387 |
|
%% returns what the func returned or just ok if command spec tells so |
388 |
|
-spec check_and_execute(caller(), t(), args()) -> success() | failure() | ignore_result. |
389 |
|
check_and_execute(Caller, Command, Args) when is_map(Args) -> |
390 |
125 |
Args1 = add_defaults(Args, Command#mongoose_command.optargs), |
391 |
125 |
ArgList = maps_to_list(Args1, Command#mongoose_command.args, Command#mongoose_command.optargs), |
392 |
125 |
check_and_execute(Caller, Command, ArgList); |
393 |
|
check_and_execute(Caller, Command, Args) -> |
394 |
|
% check permissions |
395 |
125 |
case is_available_for(Caller, Command) of |
396 |
|
true -> |
397 |
125 |
ok; |
398 |
|
false -> |
399 |
:-( |
throw({denied, "Command not available for this user"}) |
400 |
|
end, |
401 |
|
% check caller (if it is given in args, and the engine is called by a 'real' user, then it |
402 |
|
% must match |
403 |
125 |
check_caller(Caller, Command, Args), |
404 |
|
% check args |
405 |
|
% this is the 'real' spec of command - optional args included |
406 |
125 |
FullSpec = Command#mongoose_command.args |
407 |
:-( |
++ [{K, T} || {K, T, _} <- Command#mongoose_command.optargs], |
408 |
125 |
SpecLen = length(FullSpec), |
409 |
125 |
ALen = length(Args), |
410 |
125 |
case SpecLen =/= ALen of |
411 |
|
true -> |
412 |
:-( |
type_error(argument, "Invalid number of arguments: should be ~p, got ~p", [SpecLen, ALen]); |
413 |
125 |
_ -> ok |
414 |
|
end, |
415 |
125 |
[check_type(argument, S, A) || {S, A} <- lists:zip(FullSpec, Args)], |
416 |
|
% run command |
417 |
125 |
Res = apply(Command#mongoose_command.module, Command#mongoose_command.function, Args), |
418 |
125 |
case Res of |
419 |
|
{error, Type, Reason} -> |
420 |
30 |
{error, Type, Reason}; |
421 |
|
_ -> |
422 |
95 |
case Command#mongoose_command.result of |
423 |
|
ok -> |
424 |
38 |
ignore_result; |
425 |
|
ResSpec -> |
426 |
57 |
check_type(return, ResSpec, Res), |
427 |
56 |
{ok, Res} |
428 |
|
end |
429 |
|
end. |
430 |
|
|
431 |
|
check_type(_, ok, _) -> |
432 |
:-( |
ok; |
433 |
|
check_type(_, A, A) -> |
434 |
14 |
true; |
435 |
|
check_type(_, {_Name, boolean}, Value) when is_boolean(Value) -> |
436 |
1 |
true; |
437 |
|
check_type(Mode, {Name, boolean}, Value) -> |
438 |
:-( |
type_error(Mode, "For ~p expected boolean, got ~p", [Name, Value]); |
439 |
|
check_type(_, {_Name, binary}, Value) when is_binary(Value) -> |
440 |
303 |
true; |
441 |
|
check_type(Mode, {Name, binary}, Value) -> |
442 |
1 |
type_error(Mode, "For ~p expected binary, got ~p", [Name, Value]); |
443 |
|
check_type(_, {_Name, integer}, Value) when is_integer(Value) -> |
444 |
:-( |
true; |
445 |
|
check_type(Mode, {Name, integer}, Value) -> |
446 |
:-( |
type_error(Mode, "For ~p expected integer, got ~p", [Name, Value]); |
447 |
|
check_type(Mode, {_Name, [_] = LSpec}, Value) when is_list(Value) -> |
448 |
2 |
check_type(Mode, LSpec, Value); |
449 |
|
check_type(Mode, Spec, Value) when is_tuple(Spec) and not is_tuple(Value) -> |
450 |
:-( |
type_error(Mode, "~p is not a tuple", [Value]); |
451 |
|
check_type(Mode, Spec, Value) when is_tuple(Spec) -> |
452 |
:-( |
compare_tuples(Mode, Spec, Value); |
453 |
|
check_type(_, [_Spec], []) -> |
454 |
2 |
true; |
455 |
|
check_type(Mode, [Spec], [H|T]) -> |
456 |
5 |
check_type(Mode, {none, Spec}, H), |
457 |
5 |
check_type(Mode, [Spec], T); |
458 |
|
check_type(_, [], [_|_]) -> |
459 |
31 |
true; |
460 |
|
check_type(_, [], []) -> |
461 |
:-( |
true; |
462 |
|
check_type(Mode, Spec, Value) -> |
463 |
:-( |
type_error(Mode, "Catch-all: ~p vs ~p", [Spec, Value]). |
464 |
|
|
465 |
|
compare_tuples(Mode, Spec, Val) -> |
466 |
:-( |
Ssize = tuple_size(Spec), |
467 |
:-( |
Vsize = tuple_size(Val), |
468 |
:-( |
case Ssize of |
469 |
|
Vsize -> |
470 |
:-( |
compare_lists(Mode, tuple_to_list(Spec), tuple_to_list(Val)); |
471 |
|
_ -> |
472 |
:-( |
type_error(Mode, "Tuples of different size: ~p and ~p", [Spec, Val]) |
473 |
|
end. |
474 |
|
|
475 |
|
compare_lists(_, [], []) -> |
476 |
:-( |
true; |
477 |
|
compare_lists(Mode, [S|Sp], [V|Val]) -> |
478 |
:-( |
check_type(Mode, S, V), |
479 |
:-( |
compare_lists(Mode, Sp, Val). |
480 |
|
|
481 |
|
type_error(argument, Fmt, V) -> |
482 |
:-( |
throw({type_error, io_lib:format(Fmt, V)}); |
483 |
|
type_error(return, Fmt, V) -> |
484 |
1 |
throw({internal, io_lib:format(Fmt, V)}). |
485 |
|
|
486 |
|
check_identifiers(update, [], _) -> |
487 |
:-( |
baddef(identifiers, empty); |
488 |
|
check_identifiers(update, Ids, Args) -> |
489 |
3086 |
check_identifiers(Ids, Args); |
490 |
|
check_identifiers(_, _, _) -> |
491 |
13584 |
ok. |
492 |
|
|
493 |
|
check_identifiers([], _) -> |
494 |
3086 |
ok; |
495 |
|
check_identifiers([H|T], Args) -> |
496 |
4940 |
case proplists:get_value(H, Args) of |
497 |
:-( |
undefined -> baddef(H, missing); |
498 |
4940 |
_ -> check_identifiers(T, Args) |
499 |
|
end. |
500 |
|
|
501 |
|
check_command(Cmd, _PL, []) -> |
502 |
16670 |
Cmd; |
503 |
|
check_command(Cmd, PL, [N|Tail]) -> |
504 |
216710 |
V = proplists:get_value(N, PL), |
505 |
216710 |
Val = check_value(N, V), |
506 |
216710 |
check_command([Val|Cmd], PL, Tail). |
507 |
|
|
508 |
|
check_value(name, V) when is_atom(V) -> |
509 |
16670 |
V; |
510 |
|
check_value(category, V) when is_binary(V) -> |
511 |
16670 |
V; |
512 |
|
check_value(subcategory, V) when is_binary(V) -> |
513 |
4932 |
V; |
514 |
|
check_value(subcategory, undefined) -> |
515 |
11738 |
undefined; |
516 |
|
check_value(desc, V) when is_binary(V) -> |
517 |
16670 |
V; |
518 |
|
check_value(module, V) when is_atom(V) -> |
519 |
16670 |
V; |
520 |
|
check_value(function, V) when is_atom(V) -> |
521 |
16670 |
V; |
522 |
|
check_value(action, read) -> |
523 |
3708 |
read; |
524 |
|
check_value(action, send) -> |
525 |
:-( |
send; |
526 |
|
check_value(action, create) -> |
527 |
6172 |
create; |
528 |
|
check_value(action, update) -> |
529 |
3086 |
update; |
530 |
|
check_value(action, delete) -> |
531 |
3704 |
delete; |
532 |
|
check_value(args, V) when is_list(V) -> |
533 |
16670 |
Filtered = [C || {C, _} <- V], |
534 |
16670 |
ArgCount = length(V), |
535 |
16670 |
case length(Filtered) of |
536 |
16670 |
ArgCount -> V; |
537 |
:-( |
_ -> baddef(args, V) |
538 |
|
end; |
539 |
|
check_value(security_policy, undefined) -> |
540 |
11108 |
[admin]; |
541 |
|
check_value(security_policy, []) -> |
542 |
:-( |
baddef(security_policy, empty); |
543 |
|
check_value(security_policy, V) when is_list(V) -> |
544 |
5562 |
lists:map(fun check_security_policy/1, V), |
545 |
5562 |
V; |
546 |
|
check_value(result, undefined) -> |
547 |
:-( |
baddef(result, undefined); |
548 |
|
check_value(result, V) -> |
549 |
16670 |
V; |
550 |
|
check_value(identifiers, undefined) -> |
551 |
8656 |
[]; |
552 |
|
check_value(identifiers, V) -> |
553 |
8014 |
V; |
554 |
|
check_value(optargs, undefined) -> |
555 |
15434 |
[]; |
556 |
|
check_value(optargs, V) -> |
557 |
1236 |
V; |
558 |
|
check_value(caller_pos, _) -> |
559 |
16670 |
0; |
560 |
|
check_value(K, V) -> |
561 |
:-( |
baddef(K, V). |
562 |
|
|
563 |
|
%% @doc Known security policies |
564 |
|
check_security_policy(user) -> |
565 |
5562 |
ok; |
566 |
|
check_security_policy(admin) -> |
567 |
:-( |
ok; |
568 |
|
check_security_policy(Other) -> |
569 |
:-( |
baddef(security_policy, Other). |
570 |
|
|
571 |
|
baddef(K, V) -> |
572 |
:-( |
throw({invalid_command_definition, io_lib:format("~p=~p", [K, V])}). |
573 |
|
|
574 |
|
command_list(Category, Action, SubCategory) -> |
575 |
2615 |
Cmds = [C || [C] <- ets:match(mongoose_commands, '$1')], |
576 |
2615 |
filter_commands(Category, Action, SubCategory, Cmds). |
577 |
|
|
578 |
|
filter_commands(any, any, _, Cmds) -> |
579 |
231 |
Cmds; |
580 |
|
filter_commands(Cat, any, _, Cmds) -> |
581 |
2282 |
[C || C <- Cmds, C#mongoose_command.category == Cat]; |
582 |
|
filter_commands(any, _, _, _) -> |
583 |
:-( |
throw({invalid_filter, ""}); |
584 |
|
filter_commands(Cat, Action, any, Cmds) -> |
585 |
:-( |
[C || C <- Cmds, C#mongoose_command.category == Cat, |
586 |
:-( |
C#mongoose_command.action == Action]; |
587 |
|
filter_commands(Cat, Action, SubCategory, Cmds) -> |
588 |
102 |
[C || C <- Cmds, C#mongoose_command.category == Cat, |
589 |
465 |
C#mongoose_command.action == Action, |
590 |
161 |
C#mongoose_command.subcategory == SubCategory]. |
591 |
|
|
592 |
|
|
593 |
|
%% @doc make sure the command may be registered |
594 |
|
%% it may not if either (a) command of that name is already registered, |
595 |
|
%% (b) there is a command in the same category and subcategory with the same action and arity |
596 |
|
check_registration(Command) -> |
597 |
8335 |
Name = name(Command), |
598 |
8335 |
Cat = category(Command), |
599 |
8335 |
Act = action(Command), |
600 |
8335 |
Arity = arity(Command), |
601 |
8335 |
SubCat = subcategory(Command), |
602 |
8335 |
case ets:lookup(mongoose_commands, Name) of |
603 |
|
[] -> |
604 |
2179 |
CatLst = list(admin, Cat), |
605 |
2179 |
FCatLst = [C || C <- CatLst, action(C) == Act, |
606 |
803 |
subcategory(C) == SubCat, |
607 |
81 |
arity(C) == Arity], |
608 |
2179 |
case FCatLst of |
609 |
2179 |
[] -> ok; |
610 |
|
[C] -> |
611 |
:-( |
baddef("There is a command ~p in category ~p and subcategory ~p, action ~p", |
612 |
|
[name(C), Cat, SubCat, Act]) |
613 |
|
end; |
614 |
|
Other -> |
615 |
6156 |
?LOG_DEBUG(#{what => command_conflict, |
616 |
|
text => <<"This command is already defined">>, |
617 |
6156 |
command_name => Name, registered => Other}), |
618 |
6156 |
ok |
619 |
|
end. |
620 |
|
|
621 |
|
mapget(K, Map) -> |
622 |
290 |
try maps:get(K, Map) of |
623 |
290 |
V -> V |
624 |
|
catch |
625 |
|
error:{badkey, K} -> |
626 |
:-( |
type_error(argument, "Missing argument: ~p", [K]); |
627 |
|
error:bad_key -> |
628 |
:-( |
type_error(argument, "Missing argument: ~p", [K]) |
629 |
|
end. |
630 |
|
|
631 |
|
maps_to_list(Map, Args, Optargs) -> |
632 |
125 |
SpecLen = length(Args) + length(Optargs), |
633 |
125 |
ALen = maps:size(Map), |
634 |
125 |
case SpecLen of |
635 |
125 |
ALen -> ok; |
636 |
:-( |
_ -> type_error(argument, "Invalid number of arguments: should be ~p, got ~p", [SpecLen, ALen]) |
637 |
|
end, |
638 |
125 |
[mapget(K, Map) || {K, _} <- Args] ++ [mapget(K, Map) || {K, _, _} <- Optargs]. |
639 |
|
|
640 |
|
%% @doc Main entry point for permission control - is this command available for this user |
641 |
|
is_available_for(User, C) when is_binary(User) -> |
642 |
25 |
is_available_for(jid:from_binary(User), C); |
643 |
|
is_available_for(admin, _C) -> |
644 |
10373 |
true; |
645 |
|
is_available_for(Jid, #mongoose_command{security_policy = Policies}) -> |
646 |
25 |
apply_policies(Policies, Jid). |
647 |
|
|
648 |
|
%% @doc Check all security policies defined in the command - passes if any of them returns true |
649 |
|
apply_policies([], _) -> |
650 |
:-( |
false; |
651 |
|
apply_policies([P|Policies], Jid) -> |
652 |
25 |
case apply_policy(P, Jid) of |
653 |
|
true -> |
654 |
25 |
true; |
655 |
|
false -> |
656 |
:-( |
apply_policies(Policies, Jid) |
657 |
|
end. |
658 |
|
|
659 |
|
%% @doc This is the only policy we know so far, but there will be others (like roles/acl control) |
660 |
|
apply_policy(user, _) -> |
661 |
25 |
true; |
662 |
|
apply_policy(_, _) -> |
663 |
:-( |
false. |
664 |
|
|
665 |
|
locate_caller(L) -> |
666 |
16670 |
locate_caller(1, L). |
667 |
|
|
668 |
|
locate_caller(_I, []) -> |
669 |
11108 |
0; |
670 |
|
locate_caller(I, [{caller, _}|_]) -> |
671 |
5562 |
I; |
672 |
|
locate_caller(I, [_|T]) -> |
673 |
33912 |
locate_caller(I + 1, T). |
674 |
|
|
675 |
|
check_caller(admin, _Command, _Args) -> |
676 |
100 |
ok; |
677 |
|
check_caller(_Caller, #mongoose_command{caller_pos = 0}, _Args) -> |
678 |
|
% no caller in args |
679 |
:-( |
ok; |
680 |
|
check_caller(Caller, #mongoose_command{caller_pos = CallerPos}, Args) -> |
681 |
|
% check that server and user match (we don't care about resource) |
682 |
25 |
ACaller = lists:nth(CallerPos, Args), |
683 |
25 |
CallerJid = jid:from_binary(Caller), |
684 |
25 |
ACallerJid = jid:from_binary(ACaller), |
685 |
25 |
ACal = {ACallerJid#jid.user, ACallerJid#jid.server}, |
686 |
25 |
case {CallerJid#jid.user, CallerJid#jid.server} of |
687 |
|
ACal -> |
688 |
25 |
ok; |
689 |
|
_ -> |
690 |
:-( |
throw({denied, "Caller ids do not match"}) |
691 |
|
end. |
692 |
|
|
693 |
:-( |
sizeof(#{} = M) -> maps:size(M); |
694 |
:-( |
sizeof([_|_] = L) -> length(L). |