Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REST and CLI API for domains #3058

Merged
merged 27 commits into from
Mar 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1ee8a19
Insert domain API
arcusfelis Mar 18, 2021
0a434c3
Add code for enabling/disabling domain
arcusfelis Mar 19, 2021
92fc6ba
Allow to select domain info over HTTP
arcusfelis Mar 19, 2021
d52cd34
Add API to delete domain
arcusfelis Mar 19, 2021
5a4c924
Add rest_cannot_delete_domain_without_correct_type test case
arcusfelis Mar 19, 2021
d5e5ffe
Remove unknown error matching
arcusfelis Mar 22, 2021
5256e01
Fix unused variables in mongoose_domain_h
arcusfelis Mar 22, 2021
42dd7ab
Add docs for the REST API
arcusfelis Mar 22, 2021
b53ea25
Add docs for CLI API
arcusfelis Mar 22, 2021
917e7a4
Add cli_cannot_delete_domain_without_correct_type
arcusfelis Mar 24, 2021
758f1ba
Rename mongoose_domain_h to mongoose_domain_handler
arcusfelis Mar 24, 2021
214fdb7
Add cli_cannot_insert_domain_twice_with_the_another_host_type/rest_ca…
arcusfelis Mar 24, 2021
7c72525
Add cli_cannot_insert_domain_with_unknown_host_type/rest_cannot_inser…
arcusfelis Mar 24, 2021
3e56e3d
Apply suggestions from code review
arcusfelis Mar 24, 2021
4c9b0ed
Add rest_delete_missing_domain testcase
arcusfelis Mar 25, 2021
a32b15f
Apply code review
arcusfelis Mar 25, 2021
d481deb
Start error strings with a capital letter
arcusfelis Mar 25, 2021
bc094e9
Fix a whitespace
arcusfelis Mar 25, 2021
773857b
Add body-checking tests
arcusfelis Mar 26, 2021
08f80c1
Test API when db fails or service disabled
arcusfelis Mar 26, 2021
5b0b423
Test domain_when_it_is_static
arcusfelis Mar 26, 2021
4a029d1
Log errors from REST handler
arcusfelis Mar 26, 2021
9d6540f
Add specs to service_admin_extra_domain
arcusfelis Mar 26, 2021
311d343
Add specs for mongoose_domain_handler
arcusfelis Mar 26, 2021
aee975d
Add {error, static} into insert_domain spec
arcusfelis Mar 26, 2021
0d5a911
Add cli error test cases
arcusfelis Mar 26, 2021
e7bef6d
Fix 409 code in docs
arcusfelis Mar 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
406 changes: 402 additions & 4 deletions big_tests/tests/service_domain_db_SUITE.erl

Large diffs are not rendered by default.

120 changes: 115 additions & 5 deletions doc/developers-guide/domain_management.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,120 @@ The number of seconds after an event must be deleted from the `domain_events` ta

# REST API

We must provide REST API for Add/Remove and Enable/Disable interfaces of the
MongooseIM service described in the section above.
Provides API for adding/removing and enabling/disabling domains over HTTP.

# Command Line Interfaces
Implemented by `mongoose_domain_handler` module.

Configuration example:

```toml
[[listen.http]]
ip_address = "127.0.0.1"
port = 8088
transport.num_acceptors = 10
transport.max_connections = 1024

[[listen.http.handlers.mongoose_domain_handler]]
host = "localhost"
path = "/api"
```

## Add domain

```bash
curl -v -X PUT "http://localhost:8088/api/domains/example.db" \
-H 'content-type: application/json' \
-d '{"host_type": "type1"}'
```

Result codes:

* 204 - inserted.
* 409 - domain already exists with a different host type.
* 403 - DB service disabled.
* 403 - unknown host type.
* 500 - other errors.

Example of the result body with a failure reason:

```
{"what":"unknown host type"}
```

Check the `src/domain/mongoose_domain_handler.erl` file for the exact values of the `what` field if needed.


## Delete domain

You must provide the domain's host type inside the body:

```bash
curl -v -X DELETE "http://localhost:8088/api/domains/example.db" \
-H 'content-type: application/json' \
-d '{"host_type": "type1"}'
```

Result codes:

* 204 - the domain is removed or not found.
* 403 - the domain is static.
* 403 - the DB service is disabled.
* 403 - the host type is wrong (does not match the host type in the database).
* 403 - the host type is unknown.
* 500 - other errors.


## Enable/disable domain

Provide `{"enabled": true}` as a body to enable a domain.
Provide `{"enabled": false}` as a body to disable a domain.

```bash
curl -v -X PATCH "http://localhost:8088/api/domains/example.db" \
-H 'content-type: application/json' \
-d '{"enabled": true}'
```

Result codes:

* 204 - updated.
* 404 - domain not found;
* 403 - domain is static;
* 403 - service disabled.


# Command Line Interface

Implemented by `service_admin_extra_domain` module.

Configuration example:

```toml
[services.service_admin_extra]
submods = ["node", "accounts", "sessions", "vcard", "gdpr", "upload",
"roster", "last", "private", "stanza", "stats", "domain"]
```

We must provide CLI for Add/Remove and Enable/Disable interfaces of MongooseIM
service described in the section above.
Add domain:

```
./mongooseimctl insert_domain domain host_type
```

Delete domain:

```
./mongooseimctl delete_domain domain host_type
```

Disable domain:

```
./mongooseimctl disable_domain domain
```

Enable domain:

```
./mongooseimctl enable_domain domain
```
5 changes: 4 additions & 1 deletion rel/files/mongooseim.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
[[listen.http.handlers.mongoose_api_admin]]
host = "localhost"
path = "/api"
[[listen.http.handlers.mongoose_domain_handler]]
host = "localhost"
path = "/api"

[[listen.http]]
{{#http_api_client_endpoint}}
Expand Down Expand Up @@ -199,7 +202,7 @@

[services.service_admin_extra]
submods = ["node", "accounts", "sessions", "vcard", "gdpr", "upload",
"roster", "last", "private", "stanza", "stats"]
"roster", "last", "private", "stanza", "stats", "domain"]

[services.service_mongoose_system_metrics]
initial_report = 300_000
Expand Down
2 changes: 1 addition & 1 deletion src/admin_extra/service_admin_extra.erl
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
-export([start/1, stop/0, config_spec/0]).

-define(SUBMODS, [node, accounts, sessions, vcard, roster, last,
private, stanza, stats, gdpr, upload
private, stanza, stats, gdpr, upload, domain
%, srg %% Disabled until we add mod_shared_roster
]).

Expand Down
10 changes: 5 additions & 5 deletions src/domain/mongoose_domain_api.erl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ init(Pairs, AllowedHostTypes) ->

%% Domain should be nameprepped using `jid:nameprep'.
-spec insert_domain(domain(), host_type()) ->
ok | {error, duplicate} | {error, {db_error, term()}}
ok | {error, duplicate} | {error, static} | {error, {db_error, term()}}
| {error, service_disabled} | {error, unknown_host_type}.
insert_domain(Domain, HostType) ->
case check_domain(Domain, HostType) of
Expand All @@ -46,8 +46,8 @@ delete_domain(Domain, HostType) ->
end.

-spec disable_domain(domain()) ->
ok | {error, not_found} | {error, static} | {error, duplicate}
| {error, service_disabled} | {error, unknown_host_type}.
ok | {error, not_found} | {error, static} | {error, service_disabled}
| {error, {db_error, term()}}.
disable_domain(Domain) ->
case mongoose_domain_core:is_static(Domain) of
true ->
Expand All @@ -62,8 +62,8 @@ disable_domain(Domain) ->
end.

-spec enable_domain(domain()) ->
ok | {error, not_found} | {error, static} | {error, duplicate}
| {error, service_disabled} | {error, unknown_host_type}.
ok | {error, not_found} | {error, static} | {error, service_disabled}
| {error, {db_error, term()}}.
enable_domain(Domain) ->
case mongoose_domain_core:is_static(Domain) of
true ->
Expand Down
162 changes: 162 additions & 0 deletions src/domain/mongoose_domain_handler.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
%% REST API for domain actions.
-module(mongoose_domain_handler).
-behaviour(cowboy_rest).

%% ejabberd_cowboy exports
-export([cowboy_router_paths/2]).

%% Standard cowboy_rest callbacks.
-export([init/2,
allowed_methods/2,
content_types_accepted/2,
content_types_provided/2,
delete_resource/2]).

%% Custom cowboy_rest callbacks.
-export([handle_domain/2,
to_json/2]).

-include("mongoose_logger.hrl").
-type state() :: term().

-spec cowboy_router_paths(ejabberd_cowboy:path(), ejabberd_cowboy:options()) ->
ejabberd_cowboy:implemented_result().
cowboy_router_paths(Base, _Opts) ->
[{[Base, "/domains/:domain"], ?MODULE, []}].

%% cowboy_rest callbacks:
init(Req, Opts) ->
{cowboy_rest, Req, Opts}.

allowed_methods(Req, State) ->
{[<<"GET">>, <<"PUT">>, <<"PATCH">>, <<"DELETE">>], Req, State}.

content_types_accepted(Req, State) ->
{[{{<<"application">>, <<"json">>, '*'}, handle_domain}],
Req, State}.

content_types_provided(Req, State) ->
{[{{<<"application">>, <<"json">>, '*'}, to_json}], Req, State}.

%% Custom cowboy_rest callbacks:
-spec to_json(Req, State) -> {Body, Req, State} | {stop, Req, State}
when Req :: cowboy_req:req(), State :: state(), Body :: binary().
to_json(Req, State) ->
ExtDomain = cowboy_req:binding(domain, Req),
Domain = jid:nameprep(ExtDomain),
case mongoose_domain_sql:select_domain(Domain) of
{ok, Props} ->
{jiffy:encode(Props), Req, State};
{error, not_found} ->
{stop, reply_error(404, <<"domain not found">>, Req), State}
end.

-spec handle_domain(Req, State) -> {boolean(), Req, State}
when Req :: cowboy_req:req(), State :: state().
handle_domain(Req, State) ->
Method = cowboy_req:method(Req),
ExtDomain = cowboy_req:binding(domain, Req),
Domain = jid:nameprep(ExtDomain),
{ok, Body, Req2} = cowboy_req:read_body(Req),
MaybeParams = json_decode(Body),
case Method of
<<"PUT">> ->
insert_domain(Domain, MaybeParams, Req2, State);
<<"PATCH">> ->
patch_domain(Domain, MaybeParams, Req2, State)
end.

%% Private helper functions:
insert_domain(Domain, {ok, #{<<"host_type">> := HostType}}, Req, State) ->
case mongoose_domain_api:insert_domain(Domain, HostType) of
ok ->
{true, Req, State};
{error, duplicate} ->
{false, reply_error(409, <<"duplicate">>, Req), State};
{error, static} ->
{false, reply_error(403, <<"domain is static">>, Req), State};
{error, {db_error, _}} ->
{false, reply_error(500, <<"database error">>, Req), State};
{error, service_disabled} ->
{false, reply_error(403, <<"service disabled">>, Req), State};
{error, unknown_host_type} ->
{false, reply_error(403, <<"unknown host type">>, Req), State}
end;
insert_domain(_Domain, {ok, #{}}, Req, State) ->
{false, reply_error(400, <<"'host_type' field is missing">>, Req), State};
insert_domain(_Domain, {error, empty}, Req, State) ->
{false, reply_error(400, <<"body is empty">>, Req), State};
insert_domain(_Domain, {error, _}, Req, State) ->
{false, reply_error(400, <<"failed to parse JSON">>, Req), State}.

patch_domain(Domain, {ok, #{<<"enabled">> := true}}, Req, State) ->
Res = mongoose_domain_api:enable_domain(Domain),
handle_enabled_result(Res, Req, State);
patch_domain(Domain, {ok, #{<<"enabled">> := false}}, Req, State) ->
Res = mongoose_domain_api:disable_domain(Domain),
handle_enabled_result(Res, Req, State);
patch_domain(_Domain, {ok, #{}}, Req, State) ->
{false, reply_error(400, <<"'enabled' field is missing">>, Req), State};
patch_domain(_Domain, {error, empty}, Req, State) ->
{false, reply_error(400, <<"body is empty">>, Req), State};
patch_domain(_Domain, {error, _}, Req, State) ->
{false, reply_error(400, <<"failed to parse JSON">>, Req), State}.

handle_enabled_result(Res, Req, State) ->
case Res of
ok ->
{true, Req, State};
{error, not_found} ->
{false, reply_error(404, <<"domain not found">>, Req), State};
{error, static} ->
{false, reply_error(403, <<"domain is static">>, Req), State};
{error, service_disabled} ->
{false, reply_error(403, <<"service disabled">>, Req), State};
{error, {db_error, _}} ->
{false, reply_error(500, <<"database error">>, Req), State}
end.

delete_resource(Req, State) ->
ExtDomain = cowboy_req:binding(domain, Req),
Domain = jid:nameprep(ExtDomain),
{ok, Body, Req2} = cowboy_req:read_body(Req),
MaybeParams = json_decode(Body),
delete_domain(Domain, MaybeParams, Req2, State).

delete_domain(Domain, {ok, #{<<"host_type">> := HostType}}, Req, State) ->
case mongoose_domain_api:delete_domain(Domain, HostType) of
ok ->
{true, Req, State};
{error, {db_error, _}} ->
{false, reply_error(500, <<"database error">>, Req), State};
{error, static} ->
{false, reply_error(403, <<"domain is static">>, Req), State};
{error, service_disabled} ->
{false, reply_error(403, <<"service disabled">>, Req), State};
{error, wrong_host_type} ->
{false, reply_error(403, <<"wrong host type">>, Req), State};
{error, unknown_host_type} ->
{false, reply_error(403, <<"unknown host type">>, Req), State}
end;
delete_domain(_Domain, {ok, #{}}, Req, State) ->
{false, reply_error(400, <<"'host_type' field is missing">>, Req), State};
delete_domain(_Domain, {error, empty}, Req, State) ->
{false, reply_error(400, <<"body is empty">>, Req), State};
delete_domain(_Domain, {error, _}, Req, State) ->
{false, reply_error(400, <<"failed to parse JSON">>, Req), State}.

reply_error(Code, What, Req) ->
?LOG_ERROR(#{what => rest_domain_failed, reason => What,
code => Code, req => Req}),
Body = jiffy:encode(#{what => What}),
cowboy_req:reply(Code, #{<<"content-type">> => <<"application/json">>}, Body, Req).

json_decode(<<>>) ->
{error, empty};
json_decode(Bin) ->
try
{ok, jiffy:decode(Bin, [return_maps])}
catch
Class:Reason ->
{error, {Class, Reason}}
end.
Loading