1%% @author Couchbase <info@couchbase.com>
2%% @copyright 2018 Couchbase, Inc.
3%%
4%% Licensed under the Apache License, Version 2.0 (the "License");
5%% you may not use this file except in compliance with the License.
6%% You may obtain a copy of the License at
7%%
8%%      http://www.apache.org/licenses/LICENSE-2.0
9%%
10%% Unless required by applicable law or agreed to in writing, software
11%% distributed under the License is distributed on an "AS IS" BASIS,
12%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13%% See the License for the specific language governing permissions and
14%% limitations under the License.
15%%
16%% @doc handlers for audit related REST API's
17
18-module(menelaus_web_audit).
19
20-include("cut.hrl").
21-include("ns_common.hrl").
22
23-export([handle_get/1,
24         handle_post/1,
25         handle_get_descriptors/1]).
26
27handle_get(Req) ->
28    menelaus_util:assert_is_enterprise(),
29
30    Props = pre_process_get(ns_audit_cfg:get_global()),
31
32    Json =
33        lists:filtermap(fun ({K, V}) ->
34                                case key_config_to_api(K) of
35                                    undefined ->
36                                        false;
37                                    ApiK ->
38                                        {true,
39                                         {ApiK, (jsonifier(K))(V)}}
40                                end
41                        end, Props),
42
43    menelaus_util:reply_json(Req, {Json}).
44
45handle_post(Req) ->
46    menelaus_util:assert_is_enterprise(),
47
48    Config = ns_config:get(),
49    validator:handle(
50      fun (Values) ->
51              Settings = [{key_api_to_config(ApiK), V} ||
52                             {ApiK, V} <- pre_process_post(Config, Values)],
53
54              case proplists:get_bool(auditd_enabled, Settings) of
55                  true ->
56                      ns_audit_cfg:sync_set_global(Settings),
57                      audit_modify_audit_settings(Req, Settings);
58                  false ->
59                      audit_modify_audit_settings(Req, Settings),
60                      ns_audit_cfg:set_global(Settings)
61              end,
62              menelaus_util:reply(Req, 200)
63      end, Req, form, validators(Config)).
64
65audit_modify_audit_settings(Req, Settings) ->
66    case cluster_compat_mode:is_cluster_55() of
67        true -> ns_audit:modify_audit_settings(Req, Settings);
68        false -> ok
69    end.
70
71handle_get_descriptors(Req) ->
72    menelaus_util:assert_is_enterprise(),
73    menelaus_util:assert_is_55(),
74
75    Descriptors = ns_audit_cfg:get_descriptors(ns_config:latest()),
76    Json =
77        lists:map(
78          fun ({Id, Props}) ->
79                  {[{id, Id},
80                    {name, proplists:get_value(name, Props)},
81                    {module, proplists:get_value(module, Props)},
82                    {description, proplists:get_value(description, Props)}]}
83          end, Descriptors),
84    menelaus_util:reply_json(Req, Json).
85
86audit_user_exists(Identity) ->
87    SpecIds = [{N, local} || N <- memcached_permissions:spec_users()],
88    menelaus_users:user_exists(Identity) orelse lists:member(Identity, SpecIds).
89
90jsonifier(disabled_users) ->
91    fun (UList) ->
92            [{[{name, list_to_binary(N)}, {domain, D}]} ||
93                 {N, D} = Identity <- UList,
94                 audit_user_exists(Identity)]
95    end;
96jsonifier(uid) ->
97    fun list_to_binary/1;
98jsonifier(Key) ->
99    ns_audit_cfg:jsonifier(Key).
100
101key_api_to_config(auditdEnabled) ->
102    auditd_enabled;
103key_api_to_config(rotateInterval) ->
104    rotate_interval;
105key_api_to_config(rotateSize) ->
106    rotate_size;
107key_api_to_config(logPath) ->
108    log_path;
109key_api_to_config(disabledUsers) ->
110    disabled_users;
111key_api_to_config(X) when is_atom(X) ->
112    X.
113
114key_config_to_api(auditd_enabled) ->
115    auditdEnabled;
116key_config_to_api(rotate_interval) ->
117    rotateInterval;
118key_config_to_api(rotate_size) ->
119    rotateSize;
120key_config_to_api(log_path) ->
121    logPath;
122key_config_to_api(X) ->
123    case cluster_compat_mode:is_cluster_55() of
124        true ->
125            key_config_to_api_55(X);
126        false ->
127            undefined
128    end.
129
130key_config_to_api_55(actually_disabled) ->
131    disabled;
132key_config_to_api_55(disabled_users) ->
133    disabledUsers;
134key_config_to_api_55(uid) ->
135    uid;
136key_config_to_api_55(_) ->
137    undefined.
138
139pre_process_get(Props) ->
140    case cluster_compat_mode:is_cluster_55() of
141        true ->
142            Enabled = proplists:get_value(enabled, Props),
143            Disabled = proplists:get_value(disabled, Props),
144            Descriptors = ns_audit_cfg:get_descriptors(ns_config:latest()),
145
146            %% though POST API stores all configurable events as either enabled
147            %% or disabled, we anticipate that the list of configurable events
148            %% might change
149            ActuallyDisabled =
150                lists:filtermap(
151                  fun ({Id, P}) ->
152                          IsEnabledByDefault = proplists:get_value(enabled, P),
153                          case lists:member(Id, Enabled) orelse
154                              (IsEnabledByDefault andalso
155                               not lists:member(Id, Disabled)) of
156                              true ->
157                                  false;
158                              false ->
159                                  {true, Id}
160                          end
161                  end, Descriptors),
162
163            [{actually_disabled, ActuallyDisabled} | Props];
164        false ->
165            Props
166    end.
167
168pre_process_post(Config, Props) ->
169    case cluster_compat_mode:is_cluster_55(Config) of
170        true ->
171            case proplists:get_value(disabled, Props) of
172                undefined ->
173                    Props;
174                Disabled ->
175                    Descriptors =
176                        ns_audit_cfg:get_descriptors(Config),
177
178                    %% all configurable events are stored either in enabled or
179                    %% disabled list, to reduce an element of surprise in case
180                    %% if the defaults will change after the upgrade
181                    Enabled = [Id || {Id, _} <- Descriptors] -- Disabled,
182                    misc:update_proplist(Props,
183                                         [{enabled, Enabled},
184                                          {disabled, lists:sort(Disabled)}])
185            end;
186        false ->
187            Props
188    end.
189
190validate_events(Name, Descriptors, State) ->
191    validator:validate(
192      fun (Value) ->
193              Events = string:tokens(Value, ","),
194              IntEvents = [(catch list_to_integer(E)) || E <- Events],
195              case lists:all(fun is_integer/1, IntEvents) of
196                  true ->
197                      case lists:filter(orddict:is_key(_, Descriptors),
198                                        IntEvents) of
199                          IntEvents ->
200                              {value, IntEvents};
201                          Other ->
202                              BadEvents =
203                                  string:join(
204                                    [integer_to_list(E) ||
205                                        E <- IntEvents -- Other], ","),
206                              {error,
207                               io_lib:format(
208                                 "Following events are either unknown or not "
209                                 "modifiable: ~s", [BadEvents])}
210                      end;
211                  false ->
212                      {error, "All event id's must be integers"}
213              end
214      end, Name, State).
215
216validate_users(Name, State) ->
217    validator:validate(
218      fun (Value) ->
219              Users = string:tokens(Value, ","),
220              UsersParsed = [{U, string:tokens(U, "/")} || U <- Users],
221              UsersFound =
222                  lists:map(
223                    fun ({U, [N, S]}) ->
224                            Identity = {N, menelaus_web_rbac:domain_to_atom(S)},
225                            case audit_user_exists(Identity) of
226                                true ->
227                                    Identity;
228                                false ->
229                                    {error, U}
230                            end;
231                        ({U, _}) ->
232                            {error, U}
233                    end, UsersParsed),
234              case [E || {error, E} <- UsersFound] of
235                  [] ->
236                      {value, UsersFound};
237                  BadUsers ->
238                      {error,
239                       "Unrecognized users: " ++ string:join(BadUsers, ",")}
240              end
241      end, Name, State).
242
243validator_55(Config, State) ->
244    case cluster_compat_mode:is_cluster_55(Config) of
245        false ->
246            State;
247        true ->
248            functools:chain(State, validators_55(Config))
249    end.
250
251validators_55(Config) ->
252    Descriptors = orddict:from_list(ns_audit_cfg:get_descriptors(Config)),
253    [validate_events(disabled, Descriptors, _),
254     validate_users(disabledUsers, _)].
255
256validators(Config) ->
257    [validator:has_params(_),
258     validator:boolean(auditdEnabled, _),
259     validator:dir(logPath, _),
260     validator:integer(rotateInterval, _),
261     validator:range(
262       rotateInterval, 15*60, 60*60*24*7,
263       ?cut("The value must be in range from 15 minutes to 7 days"), _),
264     validator:validate(
265       fun (Value) ->
266               case Value rem 60 of
267                   0 ->
268                       ok;
269                   _ ->
270                       {error, "Value must not be a fraction of minute"}
271               end
272       end, rotateInterval, _),
273     validator:integer(rotateSize, 0, 500*1024*1024, _),
274     validator_55(Config, _),
275     validator:unsupported(_)].
276