./ct_report/coverage/service_admin_extra_gdpr.COVER.html

1 -module(service_admin_extra_gdpr).
2
3 -include("ejabberd_commands.hrl").
4 -include("mongoose_logger.hrl").
5 -include("jlib.hrl").
6
7 -export([commands/0, retrieve_all/3]).
8
9 % Exported for RPC call
10 -export([retrieve_logs/2, get_data_from_modules/2]).
11
12 -ignore_xref([commands/0, retrieve_all/3, retrieve_logs/2, get_data_from_modules/2]).
13
14 -define(CMD_TIMEOUT, 300000).
15
16 -spec commands() -> [ejabberd_commands:cmd()].
17 146 commands() -> [
18 #ejabberd_commands{name = retrieve_personal_data, tags = [gdpr],
19 desc = "Retrieve user's presonal data.",
20 longdesc = "Retrieves all personal data from MongooseIM for a given user. Example:\n"
21 " mongooseimctl retrieve_personal_data alice localhost /home/mim/alice.smith.zip ",
22 module = ?MODULE,
23 function = retrieve_all,
24 args = [{username, binary}, {domain, binary}, {path, binary}],
25 result = {res, rescode}}
26 ].
27
28 -spec retrieve_all(jid:user(), jid:server(), Path :: binary()) -> ok | {error, Reason :: any()}.
29 retrieve_all(Username, Domain, ResultFilePath) ->
30 43 JID = jid:make(Username, Domain, <<>>),
31 43 case user_exists(JID) of
32 true ->
33 42 DataFromModules = get_data_from_modules(JID),
34 % The contract is that we create personal data files only when there are any items
35 % returned for the data group.
36 42 DataToWrite = lists:filter(fun({_, _, Items}) -> Items /= [] end, DataFromModules),
37
38 42 TmpDir = make_tmp_dir(),
39
40 42 CsvFiles = lists:map(
41 fun({DataGroup, Schema, Entries}) ->
42 44 BinDataGroup = atom_to_binary(DataGroup, utf8),
43 44 FileName = <<BinDataGroup/binary, ".csv">>,
44 44 to_csv_file(FileName, Schema, Entries, TmpDir),
45 44 binary_to_list(FileName)
46 end,
47 DataToWrite),
48
49 42 LogFiles = get_all_logs(Username, Domain, TmpDir),
50
51 42 ZipFile = binary_to_list(ResultFilePath),
52 42 {ok, ZipFile} = zip:create(ZipFile, CsvFiles ++ LogFiles, [{cwd, TmpDir}]),
53 42 remove_tmp_dir(TmpDir),
54 42 ok;
55 false ->
56 1 {error, "User does not exist"}
57 end.
58
59 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
60 %%% Private funs
61 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
62
63 -spec get_data_from_modules(jid:user(), jid:server()) -> gdpr:personal_data().
64 get_data_from_modules(Username, Domain) ->
65 11 JID = jid:make(Username, Domain, <<>>),
66 11 get_data_from_modules(JID).
67
68 -spec get_data_from_modules(jid:jid()) -> gdpr:personal_data().
69 get_data_from_modules(JID) ->
70 53 {ok, HostType} = mongoose_domain_api:get_domain_host_type(JID#jid.lserver),
71 53 mongoose_hooks:get_personal_data(HostType, JID).
72
73 -spec to_csv_file(file:name_all(), gdpr:schema(), gdpr:entries(), file:name()) -> ok.
74 to_csv_file(Filename, DataSchema, DataRows, TmpDir) ->
75 44 FilePath = <<(list_to_binary(TmpDir))/binary, "/", Filename/binary>>,
76 44 {ok, File} = file:open(FilePath, [write]),
77 44 Encoded = erl_csv:encode([DataSchema | DataRows]),
78 44 file:write(File, Encoded),
79 44 file:close(File).
80
81 -spec user_exists(jid:jid()) -> boolean().
82 user_exists(JID) ->
83 43 ejabberd_auth:does_user_exist(JID).
84
85 -spec make_tmp_dir() -> file:name().
86 make_tmp_dir() ->
87 126 TmpDirName = lists:flatten(io_lib:format("/tmp/gdpr-~4.36.0b", [rand:uniform(36#zzzz)])),
88 126 case file:make_dir(TmpDirName) of
89 126 ok -> TmpDirName;
90
:-(
{error, eexist} -> make_tmp_dir();
91
:-(
{error, Error} -> {error, Error}
92 end.
93
94 -spec remove_tmp_dir(file:name()) -> ok.
95 remove_tmp_dir(TmpDir) ->
96 126 {ok, FileNames} = file:list_dir(TmpDir),
97 126 [file:delete(TmpDir ++ "/" ++ File) || File <- FileNames],
98 126 file:del_dir(TmpDir).
99
100 -type cmd() :: string() | binary().
101 -spec run(cmd(), [cmd()], timeout()) -> non_neg_integer() | timeout.
102 run(Cmd, Args, Timeout) ->
103 126 Port = erlang:open_port({spawn_executable, Cmd}, [exit_status, {args, Args}]),
104 126 receive
105 126 {Port, {exit_status, ExitStatus}} -> ExitStatus
106 after Timeout ->
107
:-(
timeout
108 end.
109
110 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
111 %%% Logs retrieval
112 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
113
114 -spec retrieve_logs(gdpr:username(), gdpr:domain()) -> {ok, ZippedLogs :: binary()}.
115 retrieve_logs(Username, Domain) ->
116 84 TmpDir = make_tmp_dir(),
117 84 LogFile = get_logs(Username, Domain, TmpDir),
118 84 {ok, {_, ZippedLogs}} = zip:create("archive.zip", [LogFile], [memory, {cwd, TmpDir}]),
119 84 remove_tmp_dir(TmpDir),
120 84 {ok, ZippedLogs}.
121
122 -spec get_all_logs(gdpr:username(), gdpr:domain(), file:name()) -> [file:name()].
123 get_all_logs(Username, Domain, TmpDir) ->
124 42 OtherNodes = mongoose_cluster:other_cluster_nodes(),
125 42 LogFile = get_logs(Username, Domain, TmpDir),
126 42 LogFilesFromOtherNodes = [get_logs_from_node(Node, Username, Domain, TmpDir) || Node <- OtherNodes],
127 42 [LogFile | LogFilesFromOtherNodes].
128
129 -spec get_logs(gdpr:username(), gdpr:domain(), file:name()) -> file:name().
130 get_logs(Username, Domain, TmpDir) ->
131 126 FileList = [filename:absname(F) || F <- mongoose_logs:get_log_files()],
132 126 Cmd = code:priv_dir(mongooseim) ++ "/parse_logs.sh",
133 126 FileName = "logs-" ++ atom_to_list(node()) ++ ".txt",
134 126 FilePath = TmpDir ++ "/" ++ FileName,
135 126 Args = [FilePath, Username, Domain | FileList],
136 126 0 = run(Cmd, Args, ?CMD_TIMEOUT),
137 126 FileName.
138
139 -spec get_logs_from_node(node(), gdpr:username(), gdpr:domain(), file:name()) -> file:name().
140 get_logs_from_node(Node, Username, Domain, TmpDir) ->
141 84 {ok, ZippedData} = rpc:call(Node, ?MODULE, retrieve_logs, [Username, Domain]),
142 84 {ok, [File]} = zip:unzip(ZippedData, [{cwd, TmpDir}]),
143 84 filename:basename(File).
Line Hits Source