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