Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4.3] Moved request validation logic out of crossbar users modules and into kazoo documents users so validation logic can be used by other parts of the code base. #6547

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions applications/crossbar/src/cb_context.erl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
,add_system_error/2, add_system_error/3, add_system_error/4
,add_validation_error/4
,validate_request_data/2, validate_request_data/3, validate_request_data/4
,add_doc_validation_errors/2, update_successfully_validated_request/2
,add_content_types_provided/2
,add_content_types_accepted/2
,add_attachment_content_type/3
Expand Down Expand Up @@ -1190,3 +1191,27 @@ system_error(Context, Error) ->
]),
_ = kz_amqp_worker:cast(Notify, fun kapi_notifications:publish_system_alert/1),
add_system_error(Error, Context).

%%------------------------------------------------------------------------------
%% @doc Add kazoo_documents validation errors to a context.
%% @end
%%------------------------------------------------------------------------------
-spec add_doc_validation_errors(context(), kazoo_documents:doc_validation_errors()) -> context().
add_doc_validation_errors(Context, ValidationErrors) ->
lists:foldl(fun({Path, Reason, Msg}, C) -> add_validation_error(Path, Reason, Msg, C) end
,Context
,ValidationErrors
).

%%------------------------------------------------------------------------------
%% @doc After successful kazoo_documents validation, update the context with
%% the updated doc and set the response status to `success'
%% @end
%%------------------------------------------------------------------------------
-spec update_successfully_validated_request(context(), kz_doc:doc()) -> context().
update_successfully_validated_request(Context, Doc) ->
Updates = [{fun set_req_data/2, Doc}
,{fun set_doc/2, Doc}
,{fun set_resp_status/2, 'success'}
],
setters(Context, Updates).
9 changes: 0 additions & 9 deletions applications/crossbar/src/crossbar_auth.erl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
,validate_auth_token/1, validate_auth_token/2
,authorize_auth_token/1
,reset_identity_secret/1
,has_identity_secret/1
,log_success_auth/4, log_success_auth/5, log_success_auth/6
,log_failed_auth/4, log_failed_auth/5, log_failed_auth/6
,get_inherited_config/1
Expand Down Expand Up @@ -185,14 +184,6 @@ reset_identity_secret(Context) ->
Doc = kz_auth_identity:reset_doc_secret(cb_context:doc(Context)),
cb_context:set_doc(Context, Doc).

%%------------------------------------------------------------------------------
%% @doc Check if user has a non-empty `pvt_signature_secret'
%% @end
%%------------------------------------------------------------------------------
-spec has_identity_secret(cb_context:context()) -> boolean().
has_identity_secret(Context) ->
kz_auth_identity:has_doc_secret(cb_context:doc(Context)).

%%------------------------------------------------------------------------------
%% @doc Get merge result of account and its parents, reseller and system
%% authentication configuration.
Expand Down
22 changes: 2 additions & 20 deletions applications/crossbar/src/crossbar_util.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1013,26 +1013,8 @@ handle_no_descendants(ViewOptions) ->
-spec format_emergency_caller_id_number(cb_context:context()) ->
cb_context:context().
format_emergency_caller_id_number(Context) ->
case cb_context:req_value(Context, [<<"caller_id">>, ?KEY_EMERGENCY]) of
'undefined' -> Context;
Emergency ->
format_emergency_caller_id_number(Context, Emergency)
end.

-spec format_emergency_caller_id_number(cb_context:context(), kz_json:object()) ->
cb_context:context().
format_emergency_caller_id_number(Context, Emergency) ->
case kz_json:get_ne_binary_value(<<"number">>, Emergency) of
'undefined' -> Context;
Number ->
NEmergency = kz_json:set_value(<<"number">>, knm_converters:normalize(Number), Emergency),
CallerId = cb_context:req_value(Context, <<"caller_id">>),
NCallerId = kz_json:set_value(?KEY_EMERGENCY, NEmergency, CallerId),

cb_context:set_req_data(Context
,kz_json:set_value(<<"caller_id">>, NCallerId, cb_context:req_data(Context))
)
end.
Doc = cb_context:req_data(Context),
cb_context:set_req_data(Context, kzd_module_utils:maybe_normalize_emergency_caller_id_number(Doc)).

-type refresh_type() :: 'user' | 'device' | 'sys_info' | 'account'.

Expand Down
34 changes: 11 additions & 23 deletions applications/crossbar/src/modules/cb_accounts.erl
Original file line number Diff line number Diff line change
Expand Up @@ -480,32 +480,28 @@ prepare_context(Context, AccountId, AccountDb) ->
]).

%%------------------------------------------------------------------------------
%% @doc
%% @doc Validate the request JObj passes all validation checks and add / alter
%% any required fields.
%% @end
%%------------------------------------------------------------------------------
-spec validate_request(kz_term:api_binary(), cb_context:context()) -> cb_context:context().
validate_request(AccountId, Context) ->
ReqJObj = cb_context:req_data(Context),

ParentId = get_parent_id_from_req(Context),
case kzd_accounts:validate(ParentId, AccountId, ReqJObj) of
{'true', AccountJObj} ->
update_validated_request(AccountId, Context, AccountJObj);
Context1 = cb_context:update_successfully_validated_request(Context, AccountJObj),
extra_validation(AccountId, Context1);
{'validation_errors', ValidationErrors} ->
add_validation_errors(Context, ValidationErrors);
{'system_error', Error} ->
cb_context:add_system_error(Error, Context)
cb_context:add_doc_validation_errors(Context, ValidationErrors);
{'system_error', Error} when is_atom(Error) ->
lager:info("system error validating account: ~p", [Error]),
cb_context:add_system_error(Error, Context);
{'system_error', {Error, Message}} ->
lager:info("system error validating account: ~p, ~p", [Error, Message]),
cb_context:add_system_error(Error, Message, Context)
end.

-spec update_validated_request(kz_term:api_binary(), cb_context:context(), kz_json:object()) -> cb_context:context().
update_validated_request(AccountId, Context, AccountJObj) ->
Updates = [{fun cb_context:set_req_data/2, AccountJObj}
,{fun cb_context:set_doc/2, AccountJObj}
,{fun cb_context:set_resp_status/2, 'success'}
],
Context1 = cb_context:setters(Context, Updates),
extra_validation(AccountId, Context1).

-spec get_parent_id_from_req(cb_context:context()) -> kz_term:api_ne_binary().
get_parent_id_from_req(Context) ->
case props:get_value(<<"accounts">>, cb_context:req_nouns(Context)) of
Expand All @@ -517,14 +513,6 @@ get_parent_id_from_req(Context) ->
end
end.

add_validation_errors(Context, ValidationErrors) ->
lists:foldl(fun add_validation_error/2
,Context
,ValidationErrors
).
add_validation_error({Path, Reason, Msg}, Context) ->
cb_context:add_validation_error(Path, Reason, Msg, Context).

-spec extra_validation(kz_term:ne_binary(), cb_context:context()) -> cb_context:context().
extra_validation(AccountId, Context) ->
Extra = [fun(_, C) -> maybe_import_enabled(C) end
Expand Down
6 changes: 2 additions & 4 deletions applications/crossbar/src/modules/cb_modules_util.erl
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,8 @@ bind(Module, Bindings) ->

-spec pass_hashes(kz_term:ne_binary(), kz_term:ne_binary()) -> {kz_term:ne_binary(), kz_term:ne_binary()}.
pass_hashes(Username, Password) ->
Creds = list_to_binary([Username, ":", Password]),
SHA1 = kz_term:to_hex_binary(crypto:hash('sha', Creds)),
MD5 = kz_term:to_hex_binary(crypto:hash('md5', Creds)),
{MD5, SHA1}.
kzd_module_utils:pass_hashes(Username, Password).


-spec get_devices_owned_by(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_json:objects().
get_devices_owned_by(OwnerID, DB) ->
Expand Down
187 changes: 21 additions & 166 deletions applications/crossbar/src/modules_v1/cb_users_v1.erl
Original file line number Diff line number Diff line change
Expand Up @@ -504,180 +504,35 @@ load_user_summary(Context) ->
load_user(UserId, Context) -> crossbar_doc:load(UserId, Context, ?TYPE_CHECK_OPTION(kzd_user:type())).

%%------------------------------------------------------------------------------
%% @doc
%% @doc Validate an update request.
%% @end
%%------------------------------------------------------------------------------
-spec validate_request(kz_term:api_binary(), cb_context:context()) -> cb_context:context().
validate_request(UserId, Context) ->
prepare_username(UserId, Context).

-spec validate_patch(kz_term:api_binary(), cb_context:context()) -> cb_context:context().
validate_patch(UserId, Context) ->
crossbar_doc:patch_and_validate(UserId, Context, fun validate_request/2).

-spec prepare_username(kz_term:api_binary(), cb_context:context()) -> cb_context:context().
prepare_username(UserId, Context) ->
JObj = cb_context:req_data(Context),
case kz_json:get_ne_value(<<"username">>, JObj) of
'undefined' -> check_user_name(UserId, Context);
Username ->
JObj1 = kz_json:set_value(<<"username">>, kz_term:to_lower_binary(Username), JObj),
check_user_name(UserId, cb_context:set_req_data(Context, JObj1))
end.

-spec check_user_name(kz_term:api_binary(), cb_context:context()) -> cb_context:context().
check_user_name(UserId, Context) ->
JObj = cb_context:req_data(Context),
UserName = kz_json:get_ne_value(<<"username">>, JObj),
AccountDb = cb_context:account_db(Context),
case is_username_unique(AccountDb, UserId, UserName) of
'true' ->
lager:debug("user name ~s is unique", [UserName]),
check_emergency_caller_id(UserId, Context);
'false' ->
lager:error("user name ~s is already in use", [UserName]),
Msg = kz_json:from_list(
[{<<"message">>, <<"User name already in use">>}
,{<<"cause">>, UserName}
]),
Context1 = cb_context:add_validation_error([<<"username">>], <<"unique">>, Msg, Context),
check_emergency_caller_id(UserId, Context1)
end.

-spec check_emergency_caller_id(kz_term:api_binary(), cb_context:context()) -> cb_context:context().
check_emergency_caller_id(UserId, Context) ->
Context1 = crossbar_util:format_emergency_caller_id_number(Context),
check_user_schema(UserId, Context1).

-spec is_username_unique(kz_term:api_binary(), kz_term:api_binary(), kz_term:ne_binary()) -> boolean().
is_username_unique(AccountDb, UserId, UserName) ->
ViewOptions = [{'key', UserName}],
case kz_datamgr:get_results(AccountDb, ?LIST_BY_USERNAME, ViewOptions) of
{'ok', []} -> 'true';
{'ok', [JObj|_]} -> kz_doc:id(JObj) =:= UserId;
_Else ->
lager:error("error ~p checking view ~p in ~p", [_Else, ?LIST_BY_USERNAME, AccountDb]),
'false'
end.

-spec check_user_schema(kz_term:api_binary(), cb_context:context()) -> cb_context:context().
check_user_schema(UserId, Context) ->
OnSuccess = fun(C) -> on_successful_validation(UserId, C) end,
cb_context:validate_request_data(<<"users">>, Context, OnSuccess).

-spec on_successful_validation(kz_term:api_binary(), cb_context:context()) -> cb_context:context().
on_successful_validation('undefined', Context) ->
Props = [{<<"pvt_type">>, <<"user">>}],
maybe_import_credintials('undefined'
,cb_context:set_doc(Context
,kz_json:set_values(Props, cb_context:doc(Context))
)
);
on_successful_validation(UserId, Context) ->
maybe_import_credintials(UserId, crossbar_doc:load_merge(UserId, Context, ?TYPE_CHECK_OPTION(kzd_user:type()))).

-spec maybe_import_credintials(kz_term:api_binary(), cb_context:context()) -> cb_context:context().
maybe_import_credintials(UserId, Context) ->
JObj = cb_context:doc(Context),
case kz_json:get_ne_value(<<"credentials">>, JObj) of
'undefined' -> maybe_validate_username(UserId, Context);
Creds ->
RemoveKeys = [<<"credentials">>, <<"pvt_sha1_auth">>],
C = cb_context:set_doc(Context
,kz_json:set_value(<<"pvt_md5_auth">>, Creds
,kz_json:delete_keys(RemoveKeys, JObj)
)
),
maybe_validate_username(UserId, C)
end.

-spec maybe_validate_username(kz_term:api_binary(), cb_context:context()) -> cb_context:context().
maybe_validate_username(UserId, Context) ->
NewUsername = kz_json:get_ne_value(<<"username">>, cb_context:doc(Context)),
CurrentUsername = case cb_context:fetch(Context, 'db_doc') of
'undefined' -> NewUsername;
CurrentJObj ->
kz_json:get_ne_value(<<"username">>, CurrentJObj, NewUsername)
end,
case kz_term:is_empty(NewUsername)
orelse CurrentUsername =:= NewUsername
orelse username_doc_id(NewUsername, Context)
of
%% user name is unchanged
'true' -> maybe_rehash_creds(UserId, NewUsername, Context);
%% updated user name that doesn't exist
'undefined' ->
manditory_rehash_creds(UserId, NewUsername, Context);
%% updated user name to existing, collect any further errors...
_Else ->
Msg = kz_json:from_list(
[{<<"message">>, <<"User name is not unique for this account">>}
,{<<"cause">>, NewUsername}
]),
C = cb_context:add_validation_error(<<"username">>, <<"unique">>, Msg, Context),
manditory_rehash_creds(UserId, NewUsername, C)
end.

-spec maybe_rehash_creds(kz_term:api_binary(), kz_term:api_binary(), cb_context:context()) -> cb_context:context().
maybe_rehash_creds(UserId, Username, Context) ->
case kz_json:get_ne_value(<<"password">>, cb_context:doc(Context)) of
%% No user name or hash, no creds for you!
'undefined' when Username =:= 'undefined' ->
HashKeys = [<<"pvt_md5_auth">>, <<"pvt_sha1_auth">>],
cb_context:set_doc(Context, kz_json:delete_keys(HashKeys, cb_context:doc(Context)));
%% User name without password, creds status quo
'undefined' -> Context;
%% Got a password, hope you also have a user name...
Password -> rehash_creds(UserId, Username, Password, Context)
end.

-spec manditory_rehash_creds(kz_term:api_binary(), kz_term:api_binary(), cb_context:context()) ->
cb_context:context().
manditory_rehash_creds(UserId, Username, Context) ->
case kz_json:get_ne_value(<<"password">>, cb_context:doc(Context)) of
'undefined' ->
Msg = kz_json:from_list(
[{<<"message">>, <<"The password must be provided when updating the user name">>}
]),
cb_context:add_validation_error(<<"password">>, <<"required">>, Msg, Context);
Password -> rehash_creds(UserId, Username, Password, Context)
end.

-spec rehash_creds(kz_term:api_binary(), kz_term:api_binary(), kz_term:ne_binary(), cb_context:context()) ->
cb_context:context().
rehash_creds(_UserId, 'undefined', _Password, Context) ->
Msg = kz_json:from_list(
[{<<"message">>, <<"The user name must be provided when updating the password">>}
]),
cb_context:add_validation_error(<<"username">>, <<"required">>, Msg, Context);
rehash_creds(_UserId, Username, Password, Context) ->
lager:debug("password set on doc, updating hashes for ~s", [Username]),
{MD5, SHA1} = cb_modules_util:pass_hashes(Username, Password),
JObj1 = kz_json:set_values([{<<"pvt_md5_auth">>, MD5}
,{<<"pvt_sha1_auth">>, SHA1}
], cb_context:doc(Context)),
crossbar_auth:reset_identity_secret(
cb_context:set_doc(Context, kz_json:delete_key(<<"password">>, JObj1))
).

%%------------------------------------------------------------------------------
%% @doc This function will determine if the username in the request is
%% unique or belongs to the request being made
%% @doc Validate the request JObj passes all validation checks and add / alter
%% any required fields.
%% @end
%%------------------------------------------------------------------------------
-spec username_doc_id(kz_term:api_binary(), cb_context:context()) -> kz_term:api_binary().
username_doc_id(Username, Context) ->
username_doc_id(Username, Context, cb_context:account_db(Context)).
username_doc_id(_, _, 'undefined') ->
'undefined';
username_doc_id(Username, Context, _AccountDb) ->
Username = kz_term:to_lower_binary(Username),
Context1 = crossbar_doc:load_view(?LIST_BY_USERNAME, [{'key', Username}], Context),
case cb_context:resp_status(Context1) =:= 'success'
andalso cb_context:doc(Context1)
of
[JObj] -> kz_doc:id(JObj);
_ -> 'undefined'
-spec validate_request(kz_term:api_binary(), cb_context:context()) -> cb_context:context().
validate_request(UserId, Context) ->
ReqJObj = cb_context:req_data(Context),
AccountId = cb_context:account_id(Context),

case kzd_users:validate(AccountId, UserId, ReqJObj) of
{'true', UserJObj} ->
lager:debug("successfull validated user object"),
cb_context:update_successfully_validated_request(Context, UserJObj);
{'validation_errors', ValidationErrors} ->
lager:info("validation errors on user"),
cb_context:add_doc_validation_errors(Context, ValidationErrors);
{'system_error', Error} when is_atom(Error) ->
lager:info("system error validating user: ~p", [Error]),
cb_context:add_system_error(Error, Context);
{'system_error', {Error, Message}} ->
lager:info("system error validating user: ~p, ~p", [Error, Message]),
cb_context:add_system_error(Error, Message, Context)
end.

%%------------------------------------------------------------------------------
Expand Down
Loading