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 |
164 |
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 |
21 |
JID = jid:make(Username, Domain, <<>>), |
31 |
21 |
case user_exists(JID) of |
32 |
|
true -> |
33 |
20 |
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 |
20 |
DataToWrite = lists:filter(fun({_, _, Items}) -> Items /= [] end, DataFromModules), |
37 |
|
|
38 |
20 |
TmpDir = make_tmp_dir(), |
39 |
|
|
40 |
20 |
CsvFiles = lists:map( |
41 |
|
fun({DataGroup, Schema, Entries}) -> |
42 |
49 |
BinDataGroup = atom_to_binary(DataGroup, utf8), |
43 |
49 |
FileName = <<BinDataGroup/binary, ".csv">>, |
44 |
49 |
to_csv_file(FileName, Schema, Entries, TmpDir), |
45 |
49 |
binary_to_list(FileName) |
46 |
|
end, |
47 |
|
DataToWrite), |
48 |
|
|
49 |
20 |
LogFiles = get_all_logs(Username, Domain, TmpDir), |
50 |
|
|
51 |
20 |
ZipFile = binary_to_list(ResultFilePath), |
52 |
20 |
{ok, ZipFile} = zip:create(ZipFile, CsvFiles ++ LogFiles, [{cwd, TmpDir}]), |
53 |
20 |
remove_tmp_dir(TmpDir), |
54 |
20 |
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 |
13 |
JID = jid:make(Username, Domain, <<>>), |
66 |
13 |
get_data_from_modules(JID). |
67 |
|
|
68 |
|
-spec get_data_from_modules(jid:jid()) -> gdpr:personal_data(). |
69 |
|
get_data_from_modules(JID) -> |
70 |
33 |
{ok, HostType} = mongoose_domain_api:get_domain_host_type(JID#jid.lserver), |
71 |
33 |
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 |
49 |
FilePath = <<(list_to_binary(TmpDir))/binary, "/", Filename/binary>>, |
76 |
49 |
{ok, File} = file:open(FilePath, [write]), |
77 |
49 |
Encoded = erl_csv:encode([DataSchema | DataRows]), |
78 |
49 |
file:write(File, Encoded), |
79 |
49 |
file:close(File). |
80 |
|
|
81 |
|
-spec user_exists(jid:jid()) -> boolean(). |
82 |
|
user_exists(JID) -> |
83 |
21 |
ejabberd_auth:does_user_exist(JID). |
84 |
|
|
85 |
|
-spec make_tmp_dir() -> file:name(). |
86 |
|
make_tmp_dir() -> |
87 |
60 |
TmpDirName = lists:flatten(io_lib:format("/tmp/gdpr-~4.36.0b", [rand:uniform(36#zzzz)])), |
88 |
60 |
case file:make_dir(TmpDirName) of |
89 |
60 |
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 |
60 |
{ok, FileNames} = file:list_dir(TmpDir), |
97 |
60 |
[file:delete(TmpDir ++ "/" ++ File) || File <- FileNames], |
98 |
60 |
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 |
60 |
Port = erlang:open_port({spawn_executable, Cmd}, [exit_status, {args, Args}]), |
104 |
60 |
receive |
105 |
60 |
{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 |
40 |
TmpDir = make_tmp_dir(), |
117 |
40 |
LogFile = get_logs(Username, Domain, TmpDir), |
118 |
40 |
{ok, {_, ZippedLogs}} = zip:create("archive.zip", [LogFile], [memory, {cwd, TmpDir}]), |
119 |
40 |
remove_tmp_dir(TmpDir), |
120 |
40 |
{ok, ZippedLogs}. |
121 |
|
|
122 |
|
-spec get_all_logs(gdpr:username(), gdpr:domain(), file:name()) -> [file:name()]. |
123 |
|
get_all_logs(Username, Domain, TmpDir) -> |
124 |
20 |
OtherNodes = mongoose_cluster:other_cluster_nodes(), |
125 |
20 |
LogFile = get_logs(Username, Domain, TmpDir), |
126 |
20 |
LogFilesFromOtherNodes = [get_logs_from_node(Node, Username, Domain, TmpDir) || Node <- OtherNodes], |
127 |
20 |
[LogFile | LogFilesFromOtherNodes]. |
128 |
|
|
129 |
|
-spec get_logs(gdpr:username(), gdpr:domain(), file:name()) -> file:name(). |
130 |
|
get_logs(Username, Domain, TmpDir) -> |
131 |
60 |
FileList = [filename:absname(F) || F <- mongoose_logs:get_log_files()], |
132 |
60 |
Cmd = code:priv_dir(mongooseim) ++ "/parse_logs.sh", |
133 |
60 |
FileName = "logs-" ++ atom_to_list(node()) ++ ".txt", |
134 |
60 |
FilePath = TmpDir ++ "/" ++ FileName, |
135 |
60 |
Args = [FilePath, Username, Domain | FileList], |
136 |
60 |
0 = run(Cmd, Args, ?CMD_TIMEOUT), |
137 |
60 |
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 |
40 |
{ok, ZippedData} = rpc:call(Node, ?MODULE, retrieve_logs, [Username, Domain]), |
142 |
40 |
{ok, [File]} = zip:unzip(ZippedData, [{cwd, TmpDir}]), |
143 |
40 |
filename:basename(File). |