Skip to content

Commit

Permalink
MB-22681 store bucket uuid's in bucket related roles
Browse files Browse the repository at this point in the history
...to prevent races with bucket deletion/creation

make sure that roles with incorrect uuid are ignored during
authentication

Change-Id: I59339940e8d6295c0604aa5b3278f1e90a8a2386
Reviewed-on: http://review.couchbase.org/77583
Reviewed-by: Aliaksey Artamonau <aliaksiej.artamonau@gmail.com>
Tested-by: Aliaksey Artamonau <aliaksiej.artamonau@gmail.com>
  • Loading branch information
vzasade authored and aartamonau committed May 2, 2017
1 parent 611d02e commit b5cd182
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 49 deletions.
2 changes: 1 addition & 1 deletion include/rbac.hrl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
-type rbac_identity_type() :: rejected | wrong_token | anonymous | admin | ro_admin | bucket |
external | local | local_token.
-type rbac_identity() :: {rbac_user_id(), rbac_identity_type()}.
-type rbac_role_param() :: string() | any.
-type rbac_role_param() :: string() | {string(), binary()} | any.
-type rbac_role_name() :: atom().
-type rbac_role() :: rbac_role_name() | {rbac_role_name(), nonempty_list(rbac_role_param())}.
-type rbac_user_name() :: string() | undefined.
Expand Down
46 changes: 27 additions & 19 deletions src/memcached_permissions.erl
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
-include_lib("eunit/include/eunit.hrl").

-record(state, {buckets,
param_values,
roles,
users,
cluster_admin}).
Expand Down Expand Up @@ -67,6 +68,8 @@ sync() ->
init() ->
Config = ns_config:get(),
#state{buckets = ns_bucket:get_bucket_names(ns_bucket:get_buckets(Config)),
param_values =
menelaus_roles:calculate_possible_param_values(ns_bucket:get_buckets(Config)),
users = [ns_config:search_node_prop(Config, memcached, admin_user) |
ns_config:search_node_prop(Config, memcached, other_users, [])],
roles = menelaus_roles:get_definitions(Config)}.
Expand All @@ -82,13 +85,14 @@ filter_event({rest_creds, _V}) ->
filter_event(_) ->
false.

handle_event({buckets, V}, #state{buckets = Buckets} = State) ->
handle_event({buckets, V}, #state{buckets = Buckets, param_values = ParamValues} = State) ->
Configs = proplists:get_value(configs, V),
case ns_bucket:get_bucket_names(Configs) of
Buckets ->
case {ns_bucket:get_bucket_names(Configs),
menelaus_roles:calculate_possible_param_values(Configs)} of
{Buckets, ParamValues} ->
unchanged;
NewBuckets ->
{changed, State#state{buckets = NewBuckets}}
{NewBuckets, NewParamValues} ->
{changed, State#state{buckets = NewBuckets, param_values = NewParamValues}}
end;
handle_event({user_version, _V}, State) ->
{changed, State};
Expand Down Expand Up @@ -117,11 +121,12 @@ producer(State) ->
end.

generate_45(#state{buckets = Buckets,
param_values = ParamValues,
roles = RoleDefinitions,
users = Users}) ->
Json =
{[memcached_admin_json(U, Buckets) || U <-Users] ++
generate_json_45(Buckets, RoleDefinitions)},
generate_json_45(Buckets, ParamValues, RoleDefinitions)},
menelaus_util:encode_json(Json).

refresh() ->
Expand All @@ -137,17 +142,17 @@ global_permissions(CompiledRoles) ->
{Permission, MemcachedPermission} <- global_permissions_to_check(),
menelaus_roles:is_allowed(Permission, CompiledRoles)]).

permissions_for_role(Buckets, RoleDefinitions, Role) ->
CompiledRoles = menelaus_roles:compile_roles([Role], RoleDefinitions),
permissions_for_role(Buckets, ParamValues, RoleDefinitions, Role) ->
CompiledRoles = menelaus_roles:compile_roles([Role], RoleDefinitions, ParamValues),
[{global, global_permissions(CompiledRoles)} |
[{Bucket, bucket_permissions(Bucket, CompiledRoles)} || Bucket <- Buckets]].

permissions_for_role(Buckets, RoleDefinitions, Role, RolesDict) ->
permissions_for_role(Buckets, ParamValues, RoleDefinitions, Role, RolesDict) ->
case dict:find(Role, RolesDict) of
{ok, Permissions} ->
{Permissions, RolesDict};
error ->
Permissions = permissions_for_role(Buckets, RoleDefinitions, Role),
Permissions = permissions_for_role(Buckets, ParamValues, RoleDefinitions, Role),
{Permissions, dict:store(Role, Permissions, RolesDict)}
end.

Expand All @@ -156,12 +161,12 @@ zip_permissions(Permissions, PermissionsAcc) ->
{Bucket, [Perm | PermAcc]}
end, Permissions, PermissionsAcc).

permissions_for_user(Roles, Buckets, RoleDefinitions, RolesDict) ->
permissions_for_user(Roles, Buckets, ParamValues, RoleDefinitions, RolesDict) ->
Acc0 = [{global, []} | [{Bucket, []} || Bucket <- Buckets]],
{ZippedPermissions, NewRolesDict} =
lists:foldl(fun (Role, {Acc, Dict}) ->
{Permissions, NewDict} =
permissions_for_role(Buckets, RoleDefinitions, Role, Dict),
permissions_for_role(Buckets, ParamValues, RoleDefinitions, Role, Dict),
{zip_permissions(Permissions, Acc), NewDict}
end, {Acc0, RolesDict}, Roles),
MergedPermissions = [{Bucket, lists:umerge(Perm)} || {Bucket, Perm} <- ZippedPermissions],
Expand All @@ -176,18 +181,18 @@ jsonify_user({UserName, Domain}, [{global, GlobalPermissions} | BucketPermission
memcached_admin_json(AU, Buckets) ->
jsonify_user({AU, local}, [{global, [all]} | [{Name, [all]} || Name <- Buckets]]).

generate_json_45(Buckets, RoleDefinitions) ->
generate_json_45(Buckets, ParamValues, RoleDefinitions) ->
RolesDict = dict:new(),
{Json, _} =
lists:foldl(fun (Bucket, {Acc, Dict}) ->
Roles = menelaus_roles:get_roles({Bucket, bucket}),
{Permissions, NewDict} =
permissions_for_user(Roles, Buckets, RoleDefinitions, Dict),
permissions_for_user(Roles, Buckets, ParamValues, RoleDefinitions, Dict),
{[jsonify_user({Bucket, local}, Permissions) | Acc], NewDict}
end, {[], RolesDict}, Buckets),
lists:reverse(Json).

jsonify_users(Users, Buckets, RoleDefinitions, ClusterAdmin) ->
jsonify_users(Users, Buckets, ParamValues, RoleDefinitions, ClusterAdmin) ->
?make_transducer(
begin
?yield(object_start),
Expand All @@ -198,7 +203,7 @@ jsonify_users(Users, Buckets, RoleDefinitions, ClusterAdmin) ->
EmitUser =
fun (Identity, Roles, Dict) ->
{Permissions, NewDict} =
permissions_for_user(Roles, Buckets, RoleDefinitions, Dict),
permissions_for_user(Roles, Buckets, ParamValues, RoleDefinitions, Dict),
?yield({kv, jsonify_user(Identity, Permissions)}),
NewDict
end,
Expand Down Expand Up @@ -237,17 +242,20 @@ jsonify_users(Users, Buckets, RoleDefinitions, ClusterAdmin) ->
end).

make_producer(#state{buckets = Buckets,
param_values = ParamValues,
roles = RoleDefinitions,
users = Users,
cluster_admin = ClusterAdmin}) ->
pipes:compose([menelaus_users:select_users('_'),
jsonify_users(Users, Buckets, RoleDefinitions, ClusterAdmin),
jsonify_users(Users, Buckets, ParamValues, RoleDefinitions, ClusterAdmin),
sjson:encode_extended_json([{compact, false},
{strict, false}])]).

generate_json_45_test() ->
Buckets = ["default", "test"],
RoleDefinitions = menelaus_roles:roles_45(),
BucketsConfig = [{"default", [{uuid, <<"default_id">>}]}, {"test", [{uuid, <<"test_id">>}]}],
Buckets = ns_bucket:get_bucket_names(BucketsConfig),
ParamValues = menelaus_roles:calculate_possible_param_values(BucketsConfig),

Json =
[{<<"default">>,
Expand All @@ -266,4 +274,4 @@ generate_json_45_test() ->
'Tap','Write','XattrRead', 'XattrWrite']}]}},
{privileges,[]},
{domain, local}]}}],
?assertEqual(Json, generate_json_45(Buckets, RoleDefinitions)).
?assertEqual(Json, generate_json_45(Buckets, ParamValues, RoleDefinitions)).
138 changes: 113 additions & 25 deletions src/menelaus_roles.erl
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@
is_allowed/2,
get_roles/1,
get_compiled_roles/1,
compile_roles/2,
compile_roles/3,
get_all_assignable_roles/1,
validate_roles/2]).
validate_roles/2,
calculate_possible_param_values/1]).

-spec roles_45() -> [rbac_role_def(), ...].
roles_45() ->
Expand Down Expand Up @@ -366,19 +367,36 @@ substitute_params(Params, ParamDefinitions, Permissions) ->
end, ObjectPattern), AllowedOperations}
end, Permissions).

-spec compile_roles([rbac_role()], [rbac_role_def()] | undefined) -> [rbac_compiled_role()].
compile_roles(_Roles, undefined) ->
-spec compile_params([atom()], [rbac_role_param()], rbac_all_param_values()) ->
false | [[rbac_role_param()]].
compile_params(ParamDefs, Params, AllParamValues) ->
PossibleValues = get_possible_param_values(ParamDefs, AllParamValues),
case find_matching_value(ParamDefs, Params, PossibleValues) of
false ->
false;
Values ->
strip_ids(ParamDefs, Values)
end.

-spec compile_roles([rbac_role()], [rbac_role_def()] | undefined, rbac_all_param_values()) ->
[rbac_compiled_role()].
compile_roles(_Roles, undefined, _AllParamValues) ->
%% can happen briefly after node joins the cluster
[];
compile_roles(Roles, Definitions) ->
lists:map(fun (Name) when is_atom(Name) ->
{Name, [], _Props, Permissions} = lists:keyfind(Name, 1, Definitions),
Permissions;
({Name, Params}) ->
{Name, ParamDefinitions, _Props, Permissions} =
lists:keyfind(Name, 1, Definitions),
substitute_params(Params, ParamDefinitions, Permissions)
end, Roles).
compile_roles(Roles, Definitions, AllParamValues) ->
lists:filtermap(fun (Name) when is_atom(Name) ->
{Name, [], _Props, Permissions} = lists:keyfind(Name, 1, Definitions),
{true, Permissions};
({Name, Params}) ->
{Name, ParamDefs, _Props, Permissions} =
lists:keyfind(Name, 1, Definitions),
case compile_params(ParamDefs, Params, AllParamValues) of
false ->
false;
NewParams ->
{true, substitute_params(NewParams, ParamDefs, Permissions)}
end
end, Roles).

-spec get_roles(rbac_identity()) -> [rbac_role()].
get_roles({"", wrong_token}) ->
Expand Down Expand Up @@ -422,12 +440,13 @@ get_roles({_User, local} = Identity) ->
-spec get_compiled_roles(rbac_identity()) -> [rbac_compiled_role()].
get_compiled_roles(Identity) ->
Definitions = get_definitions(),
compile_roles(get_roles(Identity), Definitions).
AllPossibleValues = calculate_possible_param_values(ns_bucket:get_buckets()),
compile_roles(get_roles(Identity), Definitions, AllPossibleValues).

calculate_possible_param_values(_Buckets, []) ->
[[]];
calculate_possible_param_values(Buckets, [bucket_name]) ->
[[any] | [[Name] || {Name, _} <- Buckets]].
[[any] | [[{Name, proplists:get_value(uuid, Props)}] || {Name, Props} <- Buckets]].

all_params_combinations() ->
[[], [bucket_name]].
Expand Down Expand Up @@ -455,7 +474,49 @@ get_all_assignable_roles(Config) ->
end, Acc, get_possible_param_values(ParamDefs, AllPossibleValues))
end, [], get_definitions_filtered_for_rest_api(Config)).

-spec validate_role(rbac_role(), [rbac_role_def()], [[rbac_role_param()]]) -> boolean().
strip_id(bucket_name, {P, _Id}) ->
P;
strip_id(bucket_name, P) ->
P.

strip_ids(ParamDefs, Params) ->
[strip_id(ParamDef, Param) || {ParamDef, Param} <- lists:zip(ParamDefs, Params)].

match_param(bucket_name, P, P) ->
true;
match_param(bucket_name, P, {P, _Id}) ->
true;
match_param(bucket_name, _, _) ->
false.

match_params([], [], []) ->
true;
match_params(ParamDefs, Params, Values) ->
case lists:dropwhile(
fun ({ParamDef, Param, Value}) ->
match_param(ParamDef, Param, Value)
end, lists:zip3(ParamDefs, Params, Values)) of
[] ->
true;
_ ->
false
end.

-spec find_matching_value([atom()], [rbac_role_param()], [[rbac_role_param()]]) ->
false | [rbac_role_param()].
find_matching_value(ParamDefs, Params, PossibleValues) ->
case lists:dropwhile(
fun (Values) ->
not match_params(ParamDefs, Params, Values)
end, PossibleValues) of
[] ->
false;
[V | _] ->
V
end.

-spec validate_role(rbac_role(), [rbac_role_def()], [[rbac_role_param()]]) ->
false | {ok, rbac_role()}.
validate_role(Role, Definitions, AllValues) when is_atom(Role) ->
validate_role(Role, [], Definitions, AllValues);
validate_role({Role, Params}, Definitions, AllValues) ->
Expand All @@ -464,19 +525,34 @@ validate_role({Role, Params}, Definitions, AllValues) ->
validate_role(Role, Params, Definitions, AllValues) ->
case lists:keyfind(Role, 1, Definitions) of
{Role, ParamsDef, _, _} when length(Params) =:= length(ParamsDef) ->
lists:member(Params, get_possible_param_values(ParamsDef, AllValues));
PossibleValues = get_possible_param_values(ParamsDef, AllValues),
case find_matching_value(ParamsDef, Params, PossibleValues) of
false ->
false;
[] ->
{ok, Role};
Expanded ->
{ok, {Role, Expanded}}
end;
_ ->
false
end.

validate_roles(Roles, Config) ->
Definitions = get_definitions_filtered_for_rest_api(Config),
AllParamValues = calculate_possible_param_values(ns_bucket:get_buckets(Config)),
UnknownRoles = [Role || Role <- Roles,
not validate_role(Role, Definitions, AllParamValues)],
{ValidatedRoles, UnknownRoles} =
lists:foldl(fun (Role, {V, U}) ->
case validate_role(Role, Definitions, AllParamValues) of
false ->
{V, [Role | U]};
{ok, R} ->
{[R | V], U}
end
end, {[], []}, Roles),
case UnknownRoles of
[] ->
ok;
{ok, ValidatedRoles};
_ ->
{error, roles_validation, UnknownRoles}
end.
Expand All @@ -493,10 +569,20 @@ object_match_test() ->
?assertEqual(true, object_match([{b, "a"}], [{b, any}])),
?assertEqual(true, object_match([{b, any}], [{b, any}])).

toy_config() ->
[[{buckets,
[{configs,
[{"test", [{uuid, <<"test_id">>}]},
{"default", [{uuid, <<"default_id">>}]}]}]}]].

compile_roles(Roles, Definitions) ->
AllPossibleValues = calculate_possible_param_values(ns_bucket:get_buckets(toy_config())),
compile_roles(Roles, Definitions, AllPossibleValues).

compile_roles_test() ->
?assertEqual([[{[{bucket, "test"}], none}]],
compile_roles([{test_role, ["test"]}],
[{test_role, [param], [], [{[{bucket, param}], none}]}])).
[{test_role, [bucket_name], [], [{[{bucket, bucket_name}], none}]}])).

admin_test() ->
Roles = compile_roles([admin], roles_45()),
Expand Down Expand Up @@ -594,12 +680,14 @@ replication_admin_test() ->
?assertEqual(true, is_allowed({[other], read}, Roles)).

validate_role_test() ->
Config = [[{buckets, [{configs, [{"test", []}]}]}]],
Config = toy_config(),
Definitions = roles_45(),
AllParamValues = calculate_possible_param_values(ns_bucket:get_buckets(Config)),
?assertEqual(true, validate_role(admin, Definitions, AllParamValues)),
?assertEqual(true, validate_role({bucket_admin, ["test"]}, Definitions, AllParamValues)),
?assertEqual(true, validate_role({views_admin, [any]}, Definitions, AllParamValues)),
?assertEqual({ok, admin}, validate_role(admin, Definitions, AllParamValues)),
?assertEqual({ok, {bucket_admin, [{"test", <<"test_id">>}]}},
validate_role({bucket_admin, ["test"]}, Definitions, AllParamValues)),
?assertEqual({ok, {views_admin, [any]}},
validate_role({views_admin, [any]}, Definitions, AllParamValues)),
?assertEqual(false, validate_role(something, Definitions, AllParamValues)),
?assertEqual(false, validate_role({bucket_admin, ["something"]}, Definitions, AllParamValues)),
?assertEqual(false, validate_role({something, ["test"]}, Definitions, AllParamValues)),
Expand Down
6 changes: 3 additions & 3 deletions src/menelaus_users.erl
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ store_user_45({UserName, external}, Props, Roles) ->
ns_config:run_txn(
fun (Config, SetFn) ->
case menelaus_roles:validate_roles(Roles, Config) of
ok ->
{ok, _} ->
Identity = {UserName, saslauthd},
Users = get_users_45(Config),
NewUsers = lists:keystore(Identity, 1, Users,
Expand Down Expand Up @@ -260,8 +260,8 @@ store_user_spock({_UserName, Domain} = Identity, Props, Password, Roles, Config)

store_user_spock_with_auth(Identity, Props, Auth, Roles, Config) ->
case menelaus_roles:validate_roles(Roles, Config) of
ok ->
store_user_spock_validated(Identity, [{roles, Roles} | Props], Auth),
{ok, NewRoles} ->
store_user_spock_validated(Identity, [{roles, NewRoles} | Props], Auth),
{commit, ok};
Error ->
{abort, Error}
Expand Down
Loading

0 comments on commit b5cd182

Please sign in to comment.