1%% @author Couchbase <info@couchbase.com>
2%% @copyright 2016-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 handling of memcached permissions file
17
18-module(memcached_permissions).
19
20-behaviour(memcached_cfg).
21
22-export([start_link/0, sync/0, spec_users/0]).
23
24%% callbacks
25-export([init/0, filter_event/1, handle_event/2, producer/1, refresh/0]).
26
27-include("ns_common.hrl").
28-include("pipes.hrl").
29
30-include_lib("eunit/include/eunit.hrl").
31
32-record(state, {buckets,
33                param_values,
34                roles,
35                users,
36                cluster_admin}).
37
38bucket_permissions_to_check(Bucket) ->
39    [{{[{bucket, Bucket}, data, docs], read},   'Read'},
40     {{[{bucket, Bucket}, data, docs], insert}, 'Insert'},
41     {{[{bucket, Bucket}, data, docs], delete}, 'Delete'},
42     {{[{bucket, Bucket}, data, docs], upsert}, 'Upsert'},
43     {{[{bucket, Bucket}, stats], read},        'SimpleStats'},
44     {{[{bucket, Bucket}, data, dcp], read},    'DcpProducer'},
45     {{[{bucket, Bucket}, data, dcp], write},   'DcpConsumer'},
46     {{[{bucket, Bucket}, data, meta], read},   'MetaRead'},
47     {{[{bucket, Bucket}, data, meta], write},  'MetaWrite'},
48     {{[{bucket, Bucket}, data, xattr], read},  'XattrRead'},
49     {{[{bucket, Bucket}, data, xattr], write}, 'XattrWrite'},
50     {{[{bucket, Bucket}, data, sxattr], read}, 'SystemXattrRead'},
51     {{[{bucket, Bucket}, data, sxattr], write},'SystemXattrWrite'}].
52
53global_permissions_to_check() ->
54    [{{[stats, memcached], read},           'Stats'},
55     {{[buckets], create},                  'BucketManagement'},
56     {{[admin, memcached, node], write},    'NodeManagement'},
57     {{[admin, memcached, session], write}, 'SessionManagement'},
58     {{[admin, memcached, idle], write},    'IdleConnection'},
59     {{[admin, security, audit], write},    'AuditManagement'}].
60
61start_link() ->
62    Path = ns_config:search_node_prop(ns_config:latest(), memcached, rbac_file),
63    memcached_cfg:start_link(?MODULE, Path).
64
65sync() ->
66    memcached_cfg:sync(?MODULE).
67
68init() ->
69    Config = ns_config:get(),
70    #state{buckets = ns_bucket:get_bucket_names(ns_bucket:get_buckets(Config)),
71           param_values =
72               menelaus_roles:calculate_possible_param_values(ns_bucket:get_buckets(Config)),
73           users = spec_users(Config),
74           roles = menelaus_roles:get_definitions(Config)}.
75
76spec_users() -> spec_users(ns_config:get()).
77spec_users(Config) ->
78    [ns_config:search_node_prop(Config, memcached, admin_user) |
79     ns_config:search_node_prop(Config, memcached, other_users, [])].
80
81filter_event({buckets, _V}) ->
82    true;
83filter_event({cluster_compat_version, _V}) ->
84    true;
85filter_event({user_version, _V}) ->
86    true;
87filter_event({rest_creds, _V}) ->
88    true;
89filter_event(_) ->
90    false.
91
92handle_event({buckets, V}, #state{buckets = Buckets, param_values = ParamValues} = State) ->
93    Configs = proplists:get_value(configs, V),
94    case {ns_bucket:get_bucket_names(Configs),
95          menelaus_roles:calculate_possible_param_values(Configs)} of
96        {Buckets, ParamValues} ->
97            unchanged;
98        {NewBuckets, NewParamValues} ->
99            {changed, State#state{buckets = NewBuckets, param_values = NewParamValues}}
100    end;
101handle_event({user_version, _V}, State) ->
102    {changed, State};
103handle_event({cluster_compat_version, _V}, #state{roles = Roles} = State) ->
104    case menelaus_roles:get_definitions() of
105        Roles ->
106            unchanged;
107        NewRoles ->
108            {changed, State#state{roles = NewRoles}}
109    end;
110handle_event({rest_creds, {ClusterAdmin, _}}, #state{cluster_admin = ClusterAdmin}) ->
111    unchanged;
112handle_event({rest_creds, {ClusterAdmin, _}}, State) ->
113    {changed, State#state{cluster_admin = ClusterAdmin}};
114handle_event({rest_creds, _}, #state{cluster_admin = undefined}) ->
115    unchanged;
116handle_event({rest_creds, _}, State) ->
117    {changed, State#state{cluster_admin = undefined}}.
118
119producer(State) ->
120    case cluster_compat_mode:is_cluster_50() of
121        true ->
122            make_producer(State);
123        false ->
124            ?make_producer(?yield(generate_45(State)))
125    end.
126
127generate_45(#state{buckets = Buckets,
128                   param_values = ParamValues,
129                   roles = RoleDefinitions,
130                   users = Users}) ->
131    Json =
132        {[memcached_admin_json(U) || U <- Users] ++
133             generate_json_45(Buckets, ParamValues, RoleDefinitions)},
134    menelaus_util:encode_json(Json).
135
136refresh() ->
137    memcached_refresh:refresh(rbac).
138
139bucket_permissions(Bucket, CompiledRoles) ->
140    lists:usort([MemcachedPermission ||
141                    {Permission, MemcachedPermission} <- bucket_permissions_to_check(Bucket),
142                    menelaus_roles:is_allowed(Permission, CompiledRoles)]).
143
144global_permissions(CompiledRoles) ->
145    lists:usort([MemcachedPermission ||
146                    {Permission, MemcachedPermission} <- global_permissions_to_check(),
147                    menelaus_roles:is_allowed(Permission, CompiledRoles)]).
148
149permissions_for_role(Buckets, ParamValues, RoleDefinitions, Role) ->
150    CompiledRoles = menelaus_roles:compile_roles([Role], RoleDefinitions, ParamValues),
151    [{global, global_permissions(CompiledRoles)} |
152     [{Bucket, bucket_permissions(Bucket, CompiledRoles)} || Bucket <- Buckets]].
153
154permissions_for_role(Buckets, ParamValues, RoleDefinitions, Role, RolesDict) ->
155    case dict:find(Role, RolesDict) of
156        {ok, Permissions} ->
157            {Permissions, RolesDict};
158        error ->
159            Permissions = permissions_for_role(Buckets, ParamValues, RoleDefinitions, Role),
160            {Permissions, dict:store(Role, Permissions, RolesDict)}
161    end.
162
163zip_permissions(Permissions, PermissionsAcc) ->
164    lists:zipwith(fun ({Bucket, Perm}, {Bucket, PermAcc}) ->
165                          {Bucket, [Perm | PermAcc]}
166                  end, Permissions, PermissionsAcc).
167
168permissions_for_user(Roles, Buckets, ParamValues, RoleDefinitions, RolesDict) ->
169    Acc0 = [{global, []} | [{Bucket, []} || Bucket <- Buckets]],
170    {ZippedPermissions, NewRolesDict} =
171        lists:foldl(fun (Role, {Acc, Dict}) ->
172                            {Permissions, NewDict} =
173                                permissions_for_role(Buckets, ParamValues, RoleDefinitions, Role, Dict),
174                            {zip_permissions(Permissions, Acc), NewDict}
175                    end, {Acc0, RolesDict}, Roles),
176    MergedPermissions = [{Bucket, lists:umerge(Perm)} || {Bucket, Perm} <- ZippedPermissions],
177    {MergedPermissions, NewRolesDict}.
178
179jsonify_user({UserName, Domain}, [{global, GlobalPermissions} | BucketPermissions]) ->
180    Buckets = {buckets, {[{list_to_binary(BucketName), Permissions} ||
181                             {BucketName, Permissions} <- BucketPermissions]}},
182    Global = {privileges, GlobalPermissions},
183    {list_to_binary(UserName), {[Buckets, Global, {domain, Domain}]}}.
184
185memcached_admin_json(AU) ->
186    jsonify_user({AU, local}, [{global, [all]}, {"*", [all]}]).
187
188generate_json_45(Buckets, ParamValues, RoleDefinitions) ->
189    RolesDict = dict:new(),
190    {Json, _} =
191        lists:foldl(fun (Bucket, {Acc, Dict}) ->
192                            Roles = menelaus_roles:get_roles({Bucket, bucket}),
193                            {Permissions, NewDict} =
194                                permissions_for_user(Roles, Buckets, ParamValues, RoleDefinitions, Dict),
195                            {[jsonify_user({Bucket, local}, Permissions) | Acc], NewDict}
196                    end, {[], RolesDict}, Buckets),
197    lists:reverse(Json).
198
199jsonify_users(Users, Buckets, ParamValues, RoleDefinitions, ClusterAdmin) ->
200    ?make_transducer(
201       begin
202           ?yield(object_start),
203           lists:foreach(fun (U) ->
204                                 ?yield({kv, memcached_admin_json(U)})
205                         end, Users),
206
207           EmitUser =
208               fun (Identity, Roles, Dict) ->
209                       {Permissions, NewDict} =
210                           permissions_for_user(Roles, Buckets, ParamValues, RoleDefinitions, Dict),
211                       ?yield({kv, jsonify_user(Identity, Permissions)}),
212                       NewDict
213               end,
214
215           Dict1 =
216               case ClusterAdmin of
217                   undefined ->
218                       dict:new();
219                   _ ->
220                       Roles1 = menelaus_roles:get_roles({ClusterAdmin, admin}),
221                       EmitUser({ClusterAdmin, local}, Roles1, dict:new())
222               end,
223
224           Dict2 =
225               lists:foldl(
226                 fun (Bucket, Dict) ->
227                         LegacyName = Bucket ++ ";legacy",
228                         Roles2 = menelaus_roles:get_roles({Bucket, bucket}),
229                         EmitUser({LegacyName, local}, Roles2, Dict)
230                 end, Dict1, Buckets),
231
232           pipes:fold(
233             ?producer(),
234             fun ({{user, {UserName, _} = Identity}, Props}, Dict) ->
235                     case UserName of
236                         ClusterAdmin ->
237                             TagCA = ns_config_log:tag_user_name(ClusterAdmin),
238                             ?log_warning("Encountered user ~p with the same
239                                          name as cluster administrator",
240                                          [TagCA]),
241                             Dict;
242                         _ ->
243                             Roles3 = proplists:get_value(roles, Props, []),
244                             EmitUser(Identity, Roles3, Dict)
245                     end
246             end, Dict2),
247           ?yield(object_end)
248       end).
249
250make_producer(#state{buckets = Buckets,
251                     param_values = ParamValues,
252                     roles = RoleDefinitions,
253                     users = Users,
254                     cluster_admin = ClusterAdmin}) ->
255    pipes:compose([menelaus_users:select_users('_'),
256                   jsonify_users(Users, Buckets, ParamValues, RoleDefinitions, ClusterAdmin),
257                   sjson:encode_extended_json([{compact, false},
258                                               {strict, false}])]).
259
260generate_json_45_test() ->
261    RoleDefinitions = menelaus_roles:roles_45(),
262    BucketsConfig = [{"default", [{uuid, <<"default_id">>}]}, {"test", [{uuid, <<"test_id">>}]}],
263    Buckets = ns_bucket:get_bucket_names(BucketsConfig),
264    ParamValues =  menelaus_roles:calculate_possible_param_values(BucketsConfig),
265
266    Json =
267        [{<<"default">>,
268          {[{buckets,{[{<<"default">>,
269                        ['DcpConsumer','DcpProducer','Delete','Insert',
270                         'MetaRead','MetaWrite','Read','SimpleStats',
271                         'SystemXattrRead','SystemXattrWrite','Upsert',
272                         'XattrRead','XattrWrite']},
273                       {<<"test">>,[]}]}},
274            {privileges,[]},
275            {domain, local}]}},
276         {<<"test">>,
277          {[{buckets,{[{<<"default">>,[]},
278                       {<<"test">>,
279                        ['DcpConsumer','DcpProducer','Delete','Insert',
280                         'MetaRead','MetaWrite','Read','SimpleStats',
281                         'SystemXattrRead','SystemXattrWrite','Upsert',
282                         'XattrRead','XattrWrite']}]}},
283            {privileges,[]},
284            {domain, local}]}}],
285    ?assertEqual(Json, generate_json_45(Buckets, ParamValues, RoleDefinitions)).
286