From b11ee53cd2ae94b582457e0180db06354216063d Mon Sep 17 00:00:00 2001 From: Ben Bradford Date: Fri, 1 May 2020 23:56:08 +0000 Subject: [PATCH 1/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. --- applications/crossbar/src/cb_context.erl | 25 ++ applications/crossbar/src/crossbar_auth.erl | 9 - applications/crossbar/src/crossbar_util.erl | 22 +- .../crossbar/src/modules/cb_accounts.erl | 26 +- .../crossbar/src/modules/cb_modules_util.erl | 6 +- .../crossbar/src/modules_v1/cb_users_v1.erl | 184 +------- .../crossbar/src/modules_v2/cb_users_v2.erl | 347 +-------------- .../include/kazoo_documents.hrl | 10 + core/kazoo_documents/src/kzd_accounts.erl | 116 ++--- core/kazoo_documents/src/kzd_module_utils.erl | 141 ++++++ core/kazoo_documents/src/kzd_schema.erl | 4 + core/kazoo_documents/src/kzd_users.erl | 421 +++++++++++++++++- 12 files changed, 662 insertions(+), 649 deletions(-) create mode 100644 core/kazoo_documents/src/kzd_module_utils.erl diff --git a/applications/crossbar/src/cb_context.erl b/applications/crossbar/src/cb_context.erl index 165970adcc4..280b408e1c9 100644 --- a/applications/crossbar/src/cb_context.erl +++ b/applications/crossbar/src/cb_context.erl @@ -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 @@ -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). diff --git a/applications/crossbar/src/crossbar_auth.erl b/applications/crossbar/src/crossbar_auth.erl index c020268d523..1733053cd01 100644 --- a/applications/crossbar/src/crossbar_auth.erl +++ b/applications/crossbar/src/crossbar_auth.erl @@ -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 @@ -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. diff --git a/applications/crossbar/src/crossbar_util.erl b/applications/crossbar/src/crossbar_util.erl index 08016059033..12fe4a2427d 100644 --- a/applications/crossbar/src/crossbar_util.erl +++ b/applications/crossbar/src/crossbar_util.erl @@ -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'. diff --git a/applications/crossbar/src/modules/cb_accounts.erl b/applications/crossbar/src/modules/cb_accounts.erl index de835f77a24..d53d78b9a10 100644 --- a/applications/crossbar/src/modules/cb_accounts.erl +++ b/applications/crossbar/src/modules/cb_accounts.erl @@ -480,32 +480,24 @@ 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); + cb_context:add_doc_validation_errors(Context, ValidationErrors); {'system_error', Error} -> cb_context:add_system_error(Error, 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 @@ -517,14 +509,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 diff --git a/applications/crossbar/src/modules/cb_modules_util.erl b/applications/crossbar/src/modules/cb_modules_util.erl index e7c03bfef49..116d0361283 100644 --- a/applications/crossbar/src/modules/cb_modules_util.erl +++ b/applications/crossbar/src/modules/cb_modules_util.erl @@ -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) -> diff --git a/applications/crossbar/src/modules_v1/cb_users_v1.erl b/applications/crossbar/src/modules_v1/cb_users_v1.erl index e7f49b9e4c7..74d4650bc90 100644 --- a/applications/crossbar/src/modules_v1/cb_users_v1.erl +++ b/applications/crossbar/src/modules_v1/cb_users_v1.erl @@ -504,180 +504,32 @@ 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} -> + lager:info("system error validating user: ~p", [Error]), + cb_context:add_system_error(Error, Context) end. %%------------------------------------------------------------------------------ diff --git a/applications/crossbar/src/modules_v2/cb_users_v2.erl b/applications/crossbar/src/modules_v2/cb_users_v2.erl index 093cb1621f5..e7c12a91740 100644 --- a/applications/crossbar/src/modules_v2/cb_users_v2.erl +++ b/applications/crossbar/src/modules_v2/cb_users_v2.erl @@ -460,344 +460,33 @@ fix_envelope(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_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 validate_request(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). -validate_request(UserId, Context) -> - Routines = [fun normalize_username/2 - ,fun normalize_emergency_caller_id/2 - ,fun maybe_import_credintials/2 - %% check_user_schema will load and merge the current doc as well - ,fun check_user_schema/2 - %% this check must have the current doc - ,fun maybe_set_identity_secret/2 - %% this check must have the current doc - ,fun check_username/2 - %% this check must have the current doc - ,fun check_hotdesk_id/2 - %% this check must have the current doc - ,fun maybe_rehash_creds/2 - ], - lists:foldl(fun(F, C) -> - F(UserId, C) - end - ,Context - ,Routines - ). - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ - --spec normalize_username(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). -normalize_username(_UserId, Context) -> - JObj = cb_context:req_data(Context), - case kzd_users:username(JObj) of - 'undefined' -> Context; - Username -> - NormalizedUsername = kz_term:to_lower_binary(Username), - cb_context:set_req_data(Context, kzd_users:set_username(JObj, NormalizedUsername)) - end. - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ - --spec normalize_emergency_caller_id(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). -normalize_emergency_caller_id(_UserId, Context) -> - crossbar_util:format_emergency_caller_id_number(Context). - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ - --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' -> Context; - Creds -> - RemoveKeys = [<<"credentials">>, <<"pvt_sha1_auth">>], - UpdatedJObj = - kz_json:set_value(<<"pvt_md5_auth">> - ,Creds - ,kz_json:delete_keys(RemoveKeys, JObj) - ), - cb_context:set_doc(Context, UpdatedJObj) - end. - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ - --spec maybe_set_identity_secret(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). -maybe_set_identity_secret(_UserId, Context) -> - case crossbar_auth:has_identity_secret(Context) - orelse cb_context:has_errors(Context) - of - 'true' -> Context; - 'false' -> - lager:debug("initalizing identity secret"), - crossbar_auth:reset_identity_secret(Context) - end. - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec check_username(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). -check_username(UserId, Context) -> - JObj = cb_context:req_data(Context), - Username = kzd_users:username(JObj), - CurrentJObj = cb_context:fetch(Context, 'db_doc', kz_json:new()), - CurrentUsername = kzd_users:username(CurrentJObj), - AccountDb = cb_context:account_db(Context), - case kz_term:is_empty(Username) - orelse Username =:= CurrentUsername - orelse is_username_unique(AccountDb, UserId, Username) - of - 'true' -> - lager:debug("username ~s (currently ~s) is unique" - ,[Username, CurrentUsername] - ), - Context; - 'false' -> - lager:error("username ~s (currently ~s) is already used" - ,[Username, CurrentUsername] - ), - non_unique_username_error(Context, Username) - end. - --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; - {'error', _R} -> - lager:error("error checking view ~p in ~p" - ,[?LIST_BY_USERNAME, AccountDb, _R] - ), - 'false' - end. - --spec non_unique_username_error(cb_context:context(), kz_term:ne_binary()) -> cb_context:context(). -non_unique_username_error(Context, Username) -> - Msg = kz_json:from_list( - [{<<"message">>, <<"User name is not unique for this account">>} - ,{<<"cause">>, Username} - ]), - cb_context:add_validation_error([<<"username">>], <<"unique">>, Msg, Context). - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ - --spec check_hotdesk_id(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). -check_hotdesk_id(UserId, Context) -> - JObj = cb_context:req_data(Context), - HotdeskId = kzd_users:hotdesk_id(JObj), - CurrentJObj = cb_context:fetch(Context, 'db_doc', kz_json:new()), - CurrentHotdeskId = kzd_users:hotdesk_id(CurrentJObj), - AccountDb = cb_context:account_db(Context), - case kz_term:is_empty(HotdeskId) - orelse HotdeskId =:= CurrentHotdeskId - orelse is_hotdesk_id_unique(AccountDb, UserId, HotdeskId) - of - 'true' -> - lager:debug("hotdesk ID ~s (currently ~s) is unique" - ,[HotdeskId, CurrentHotdeskId] - ), - Context; - 'false' -> - lager:debug("hotdesk ID ~s (currently ~s) is already used" - ,[HotdeskId, CurrentHotdeskId] - ), - non_unique_hotdesk_id_error(Context, HotdeskId) - end. - --spec is_hotdesk_id_unique(kz_term:api_binary(), kz_term:api_binary(), kz_term:ne_binary()) -> boolean() | kz_datamgr:data_error(). -is_hotdesk_id_unique(AccountDb, UserId, HotdeskId) -> - ViewOptions = [{'key', HotdeskId}], - case kz_datamgr:get_results(AccountDb, ?LIST_BY_HOTDESK_ID, ViewOptions) of - {'ok', []} -> 'true'; - {'ok', [JObj|_]} -> kz_doc:id(JObj) =:= UserId; - {'error', _R} -> - lager:error("error checking view ~p in ~p: ~p" - ,[?LIST_BY_HOTDESK_ID, AccountDb, _R] - ), - 'false' - end. - --spec non_unique_hotdesk_id_error(cb_context:context(), kz_term:ne_binary()) -> cb_context:context(). -non_unique_hotdesk_id_error(Context, HotdeskId) -> - Msg = kz_json:from_list( - [{<<"message">>, <<"Hotdesk ID is not unique for this account">>} - ,{<<"cause">>, HotdeskId} - ]), - cb_context:add_validation_error([<<"hotdesk">>, <<"id">>], <<"unique">>, Msg, Context). - %%------------------------------------------------------------------------------ -%% @doc +%% @doc Validate the request JObj passes all validation checks and add / alter +%% any required fields. %% @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">>, kzd_user:type()}], - JObj = kz_json:set_values(Props, cb_context:doc(Context)), - maybe_import_credintials('undefined', cb_context:set_doc(Context, JObj)); -on_successful_validation(UserId, Context) -> - maybe_import_credintials(UserId, crossbar_doc:load_merge(UserId, Context, ?TYPE_CHECK_OPTION(kzd_user:type()))). - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec maybe_rehash_creds(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). -maybe_rehash_creds(_UserId, Context) -> - case cb_context:has_errors(Context) of - 'false' -> rehash_creds(Context); - 'true' -> Context - end. - --spec rehash_creds(cb_context:context()) -> cb_context:context(). -rehash_creds(Context) -> - JObj = cb_context:doc(Context), - Username = kzd_users:username(JObj), - CurrentJObj = cb_context:fetch(Context, 'db_doc', kz_json:new()), - CurrentUsername = kzd_users:username(CurrentJObj), - Password = kz_json:get_ne_binary_value(<<"password">>, JObj), - GeneratePassword = kapps_config:get_is_true(?MOD_CONFIG_CAT, <<"generate_password_if_empty">>, 'false'), - GenerateUsername = kapps_config:get_is_true(?MOD_CONFIG_CAT, <<"generate_username_if_empty">>, 'true'), - GenerateCreds = - GenerateUsername - andalso GeneratePassword, - case - {Username =:= CurrentUsername - ,kz_term:is_empty(Username) - ,kz_term:is_empty(Password)} - of - {'false', 'false', 'false'} -> - lager:debug("requested different username (new: ~s current: ~s) with a password" - ,[Username, CurrentUsername] - ), - rehash_creds(Username, Password, Context); - {'false', 'false', 'true'} -> - lager:debug("requested different username (new: ~s current: ~s) without a password" - ,[Username, CurrentUsername] - ), - maybe_generated_password_hash(GeneratePassword, Username, Context); - {'false', 'true', 'false'} -> - lager:debug("requested no username but provided a password"), - maybe_generated_username_hash(GenerateUsername, Password, Context); - {'false', 'true', 'true'} -> - lager:debug("requested no username or password"), - maybe_generated_creds_hash(GenerateCreds, Context); - {'true', 'false', 'false'} -> - lager:debug("requested same username (new: ~s current: ~s) with a password" - ,[Username, CurrentUsername] - ), - rehash_creds(Username, Password, Context); - {'true', 'false', 'true'} -> - lager:debug("requested same username (new: ~s current: ~s) without a password" - ,[Username, CurrentUsername] - ), - Context; - {'true', 'true', 'false'} -> - lager:debug("requested no username (new: ~s current: ~s) with a password" - ,[Username, CurrentUsername] - ), - maybe_generated_username_hash(GenerateUsername, Password, Context); - {'true', 'true', 'true'} -> - lager:debug("requested no username, no current username, and no password"), - maybe_generated_creds_hash(GenerateCreds, Context) - end. - --spec maybe_generated_password_hash(boolean(), kz_term:ne_binary(), cb_context:context()) -> cb_context:context(). -maybe_generated_password_hash('false', _Username, Context) -> - 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); -maybe_generated_password_hash('true', Username, Context) -> - rehash_creds(Username, generate_password(), Context). - --spec maybe_generated_username_hash(boolean(), kz_term:ne_binary(), cb_context:context()) -> cb_context:context(). -maybe_generated_username_hash('false', _Password, Context) -> - Msg = kz_json:from_list( - [{<<"message">>, <<"The username must be provided when updating the password">>} - ]), - cb_context:add_validation_error(<<"username">>, <<"required">>, Msg, Context); -maybe_generated_username_hash('true', Password, Context) -> - Username = generate_username(), - JObj = kzd_users:set_username(cb_context:doc(Context), Username), - rehash_creds(Username, Password, cb_context:set_doc(Context, JObj)). - --spec maybe_generated_creds_hash(boolean(), cb_context:context()) -> cb_context:context(). -maybe_generated_creds_hash('false', Context) -> - remove_creds(Context); -maybe_generated_creds_hash('true', Context) -> - Username = generate_username(), - JObj = kzd_users:set_username(cb_context:doc(Context), Username), - rehash_creds(Username, generate_password(), cb_context:set_doc(Context, JObj)). - --spec generate_username() -> kz_term:ne_binary(). -generate_username() -> - lager:debug("generating random username"), - <<"user_", (kz_binary:rand_hex(8))/binary>>. - --spec generate_password() -> kz_term:ne_binary(). -generate_password() -> - lager:debug("generating random password"), - kz_binary:rand_hex(32). - --spec remove_creds(cb_context:context()) -> cb_context:context(). -remove_creds(Context) -> - lager:debug("removing user creds"), - HashKeys = [<<"pvt_md5_auth">>, <<"pvt_sha1_auth">>], - cb_context:set_doc(Context, kz_json:delete_keys(HashKeys, cb_context:doc(Context))). - --spec rehash_creds(kz_term:api_binary(), kz_term:ne_binary(), cb_context:context()) -> - cb_context:context(). -rehash_creds(Username, Password, Context) -> - lager:debug("updating cred hashes for ~s", [Username]), - CurrentJObj = cb_context:doc(Context), - CurrentMD5 = kz_json:get_ne_value(<<"pvt_md5_auth">>, CurrentJObj), - CurrentSHA1 = kz_json:get_ne_value(<<"pvt_sha1_auth">>, CurrentJObj), - {MD5, SHA1} = cb_modules_util:pass_hashes(Username, Password), - JObj = kz_json:set_values([{<<"pvt_md5_auth">>, MD5} - ,{<<"pvt_sha1_auth">>, SHA1} - ] - ,CurrentJObj - ), - case kapps_config:get_is_true(?MOD_CONFIG_CAT, <<"reset_identity_secret_on_rehash">>, 'true') - andalso (CurrentMD5 =/= MD5 - orelse CurrentSHA1 =/= SHA1) - of - 'false' -> - cb_context:set_doc(Context, kz_json:delete_key(<<"password">>, JObj)); - 'true' -> - lager:debug("resetting identity secret", []), - crossbar_auth:reset_identity_secret( - cb_context:set_doc(Context, kz_json:delete_key(<<"password">>, JObj)) - ) +-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} -> + lager:info("system error validating user: ~p", [Error]), + cb_context:add_system_error(Error, Context) end. %%------------------------------------------------------------------------------ diff --git a/core/kazoo_documents/include/kazoo_documents.hrl b/core/kazoo_documents/include/kazoo_documents.hrl index bd5487e0c2a..b7e28344045 100644 --- a/core/kazoo_documents/include/kazoo_documents.hrl +++ b/core/kazoo_documents/include/kazoo_documents.hrl @@ -18,4 +18,14 @@ -define(CURRENT_VERSION, ?VERSION_2). -define(KAZOO_DOCUMENTS_HRL, 'true'). + +-type doc_validation_error() :: {kz_json:path(), kz_term:ne_binary(), kz_json:object()}. +-type doc_validation_errors() :: [doc_validation_error()]. +-type doc_validation_acc() :: {kz_doc:doc(), doc_validation_errors()}. +-type doc_validation_fun() :: fun((kz_term:api_ne_binary(), doc_validation_acc()) -> doc_validation_acc()). +-type doc_validation_after_fun() :: fun((doc_validation_acc()) -> doc_validation_acc()) | 'undefined'. +-export_type([doc_validation_error/0, doc_validation_errors/0 + ,doc_validation_acc/0, doc_validation_fun/0 + ,doc_validation_after_fun/0 + ]). -endif. diff --git a/core/kazoo_documents/src/kzd_accounts.erl b/core/kazoo_documents/src/kzd_accounts.erl index b1a5c84cc5e..adbdaaee782 100644 --- a/core/kazoo_documents/src/kzd_accounts.erl +++ b/core/kazoo_documents/src/kzd_accounts.erl @@ -126,15 +126,6 @@ -define(AGG_VIEW_NAME, <<"accounts/listing_by_name">>). -define(ACCOUNTS_CONFIG_CAT, <<"crossbar.accounts">>). --define(CONFIG_CAT, <<"crossbar">>). - --define(SHOULD_ENSURE_SCHEMA_IS_VALID - ,kapps_config:get_is_true(?CONFIG_CAT, <<"ensure_valid_schema">>, 'true') - ). - --define(SHOULD_FAIL_ON_INVALID_DATA - ,kapps_config:get_is_true(?CONFIG_CAT, <<"schema_strict_validation">>, 'false') - ). -define(ACCOUNT_REALM_SUFFIX ,kapps_config:get_binary(?ACCOUNTS_CONFIG_CAT, <<"account_realm_suffix">>, <<"sip.2600hz.com">>) @@ -1376,11 +1367,9 @@ is_alphanumeric(_) -> %% or returns the validation error {Path, ErrorType, ErrorMessage} %% @end %%------------------------------------------------------------------------------ --type validation_error() :: {kz_json:path(), kz_term:ne_binary(), kz_json:object()}. --type validation_errors() :: [validation_error()]. -spec validate(kz_term:api_ne_binary(), kz_term:api_ne_binary(), doc()) -> {'true', doc()} | - {'validation_errors', validation_errors()} | + {'validation_errors', kazoo_documents:doc_validation_errors()} | {'system_error', atom()}. validate(ParentId, AccountId, ReqJObj) -> ValidateFuns = [fun ensure_account_has_realm/2 @@ -1399,19 +1388,16 @@ validate(ParentId, AccountId, ReqJObj) -> 'throw':SystemError -> SystemError end. --type validate_acc() :: {doc(), validation_errors()}. --type validate_fun() :: fun((kz_term:api_ne_binary(), validate_acc()) -> validate_acc()). - --spec do_validation(kz_term:api_ne_binary(), doc(), [validate_fun()]) -> +-spec do_validation(kz_term:api_ne_binary(), doc(), [kazoo_documents:doc_validation_fun()]) -> {'true', doc()} | - {'validation_errors', validation_errors()}. + {'validation_errors', kazoo_documents:doc_validation_errors()}. do_validation(AccountId, ReqJObj, ValidateFuns) -> lists:foldl(fun(F, Acc) -> F(AccountId, Acc) end ,{ReqJObj, []} ,ValidateFuns ). --spec ensure_account_has_realm(kz_term:api_ne_binary(), validate_acc()) -> validate_acc(). +-spec ensure_account_has_realm(kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). ensure_account_has_realm(_AccountId, {Doc, Errors}) -> case realm(Doc) of 'undefined' -> @@ -1423,7 +1409,7 @@ ensure_account_has_realm(_AccountId, {Doc, Errors}) -> {Doc, Errors} end. --spec ensure_account_has_timezone(kz_term:api_ne_binary(), validate_acc()) -> validate_acc(). +-spec ensure_account_has_timezone(kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). ensure_account_has_timezone(_AccountId, {Doc, Errors}) -> Timezone = timezone(Doc), lager:debug("selected timezone: ~s", [Timezone]), @@ -1433,7 +1419,7 @@ ensure_account_has_timezone(_AccountId, {Doc, Errors}) -> random_realm() -> <<(kz_binary:rand_hex(?RANDOM_REALM_STRENGTH))/binary, ".", (?ACCOUNT_REALM_SUFFIX)/binary>>. --spec remove_spaces(kz_term:api_ne_binary(), validate_acc()) -> validate_acc(). +-spec remove_spaces(kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). remove_spaces(_AccountId, {Doc, Errors}) -> {lists:foldl(fun remove_spaces_fold/2, Doc, ?REMOVE_SPACES) ,Errors @@ -1448,7 +1434,7 @@ remove_spaces_fold(Key, Doc) -> kz_json:set_value(Key, NoSpaces, Doc) end. --spec cleanup_leaky_keys(kz_term:api_ne_binary(), validate_acc()) -> validate_acc(). +-spec cleanup_leaky_keys(kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). cleanup_leaky_keys(_AccountId, {Doc, Errors}) -> RemoveKeys = [<<"wnm_allow_additions">> ,<<"superduper_admin">> @@ -1458,7 +1444,7 @@ cleanup_leaky_keys(_AccountId, {Doc, Errors}) -> ,Errors }. --spec validate_realm_is_unique(kz_term:api_ne_binary(), validate_acc()) -> validate_acc(). +-spec validate_realm_is_unique(kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). validate_realm_is_unique(AccountId, {Doc, Errors}) -> Realm = realm(Doc), case is_unique_realm(AccountId, Realm) of @@ -1489,7 +1475,7 @@ is_unique_realm(AccountId, Realm) -> _Else -> 'false' end. --spec validate_account_name_is_unique(kz_term:api_ne_binary(), validate_acc()) -> validate_acc(). +-spec validate_account_name_is_unique(kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). validate_account_name_is_unique(AccountId, {Doc, Errors}) -> Name = name(Doc), case maybe_is_unique_account_name(AccountId, Name) of @@ -1528,80 +1514,32 @@ is_unique_account_name(AccountId, Name) -> 'false' end. --spec validate_schema(kz_term:api_ne_binary(), kz_term:api_ne_binary(), validate_acc()) -> validate_acc(). +%%------------------------------------------------------------------------------ +%% @doc Verify the account doc against the account doc schema +%% @end +%%------------------------------------------------------------------------------ +-spec validate_schema(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). validate_schema(ParentId, AccountId, {Doc, Errors}) -> - lager:debug("validating payload against schema"), - SchemaRequired = ?SHOULD_ENSURE_SCHEMA_IS_VALID, - - case kz_json_schema:load(<<"accounts">>) of - {'ok', SchemaJObj} -> validate_account_schema(ParentId, AccountId, Doc, Errors, SchemaJObj); - {'error', 'not_found'} when SchemaRequired -> - lager:error("accounts schema not found and is required"), - throw({'system_error', <<"schema accounts not found.">>}); - {'error', 'not_found'} -> - lager:error("accounts schema not found, continuing anyway"), - validate_passed(ParentId, AccountId, {Doc, Errors}) - end. + OnSuccess = fun(ValidateAcc) -> on_successful_schema_validation(ParentId, AccountId, ValidateAcc) end, + kzd_module_utils:validate_schema(<<"accounts">>, {Doc, Errors}, OnSuccess). --spec validate_passed(kz_term:api_ne_binary(), kz_term:api_ne_binary(), validate_acc()) -> validate_acc(). -validate_passed(ParentId, 'undefined', {Doc, Errors}) -> - lager:info("validation passed for new account: ~s", [kz_json:encode(Doc)]), +%%------------------------------------------------------------------------------ +%% @doc Executed after `validate_user_schema/3' if it passes schema validation. +%% @end +%%------------------------------------------------------------------------------ +-spec on_successful_schema_validation(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +on_successful_schema_validation(ParentId, 'undefined', {Doc, Errors}) -> + lager:info("schema validation passed for new account: ~s", [kz_json:encode(Doc)]), {set_private_properties(ParentId, Doc), Errors}; -validate_passed(_ParentId, AccountId, {Doc, Errors}) -> +on_successful_schema_validation(_ParentId, AccountId, {Doc, Errors}) -> case update(AccountId, kz_json:to_proplist(kz_json:flatten(Doc))) of {'ok', UpdatedAccount} -> {UpdatedAccount, Errors}; {'error', _E} -> {Doc, Errors} end. --spec validate_account_schema(kz_term:api_ne_binary(), kz_term:api_ne_binary(), doc(), validation_errors(), kz_json:object()) -> - validate_acc(). -validate_account_schema(ParentId, AccountId, Doc, ValidationErrors, SchemaJObj) -> - Strict = ?SHOULD_FAIL_ON_INVALID_DATA, - SystemSL = kapps_config:get_binary(<<"crossbar">>, <<"stability_level">>), - Options = [{'extra_validator_options', [{'stability_level', SystemSL}]}], - - try kz_json_schema:validate(SchemaJObj, kz_doc:public_fields(Doc), Options) of - {'ok', JObj} -> - lager:debug("account payload is valid"), - validate_passed(ParentId, AccountId, {JObj, ValidationErrors}); - {'error', SchemaErrors} when Strict -> - lager:error("validation errors when strictly validating"), - validate_failed(Doc, ValidationErrors, SchemaErrors); - {'error', SchemaErrors} -> - lager:error("validation errors but not strictly validating, trying to fix request"), - maybe_fix_js_types(ParentId, AccountId, Doc, ValidationErrors, SchemaErrors, SchemaJObj) - catch - 'error':'function_clause' -> - ST = erlang:get_stacktrace(), - lager:error("function clause failure"), - kz_util:log_stacktrace(ST), - throw({'system_error', <<"validation failed to run on the server">>}) - end. - --spec maybe_fix_js_types(kz_term:api_ne_binary(), kz_term:api_ne_binary(), doc(), validation_errors(), [jesse_error:error_message()], kz_json:object()) -> - validate_acc(). -maybe_fix_js_types(ParentId, AccountId, Doc, ValidationErrors, SchemaErrors, SchemaJObj) -> - case kz_json_schema:fix_js_types(Doc, SchemaErrors) of - 'false' -> validate_failed(Doc, ValidationErrors, SchemaErrors); - {'true', NewDoc} -> - validate_account_schema(ParentId, AccountId, NewDoc, ValidationErrors, SchemaJObj) - end. - --spec validate_failed(doc(), validation_errors(), [jesse_error:error_reason()]) -> validate_acc(). -validate_failed(Doc, ValidationErrors, SchemaErrors) -> - {Doc - ,[validation_error(Error) || Error <- SchemaErrors] ++ ValidationErrors - }. - --spec validation_error(jesse_error:error_reason()) -> validation_error(). -validation_error(Error) -> - {_ErrorCode, ErrorMessage, ErrorJObj} = kz_json_schema:error_to_jobj(Error), - [Key] = kz_json:get_keys(ErrorJObj), - {[JObj], [_Code]} = kz_json:get_values(Key, ErrorJObj), - lager:info("adding error prop ~s ~s: ~p", [Key, ErrorMessage, JObj]), - {Key, ErrorMessage, JObj}. - --spec normalize_alphanum_name(kz_term:api_ne_binary(), validate_acc()) -> validate_acc(). +-spec normalize_alphanum_name(kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). normalize_alphanum_name(_AccountId, {Doc, Errors}) -> Normalized = normalize_name(name(Doc)), {kz_json:set_value(<<"pvt_alphanum_name">>, Normalized, Doc) diff --git a/core/kazoo_documents/src/kzd_module_utils.erl b/core/kazoo_documents/src/kzd_module_utils.erl new file mode 100644 index 00000000000..dd0ffb7bca5 --- /dev/null +++ b/core/kazoo_documents/src/kzd_module_utils.erl @@ -0,0 +1,141 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2016-, Voxter Communications +%%% @doc Utility functions for kazoo_documents +%%% @end +%%%----------------------------------------------------------------------------- +-module(kzd_module_utils). + +-include("kz_documents.hrl"). + +-export([maybe_normalize_emergency_caller_id_number/1 + ,pass_hashes/2 + ,validate_schema/3 + ]). + +-define(KEY_EMERGENCY_NUMBER, [<<"caller_id">>, <<"emergency">>, <<"number">>]). + +-define(CROSSBAR_CONFIG_CAT, <<"crossbar">>). + +-define(SHOULD_ENSURE_SCHEMA_IS_VALID + ,kapps_config:get_is_true(?CROSSBAR_CONFIG_CAT, <<"ensure_valid_schema">>, 'true') + ). +-define(SHOULD_FAIL_ON_INVALID_DATA + ,kapps_config:get_is_true(?CROSSBAR_CONFIG_CAT, <<"schema_strict_validation">>, 'false') + ). +-define(CROSSBAR_STABILITY_LEVEL + ,kapps_config:get_binary(?CROSSBAR_CONFIG_CAT, <<"stability_level">>) + ). + +%%------------------------------------------------------------------------------ +%% @doc If set, normalize the doc's emergency caller id. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_normalize_emergency_caller_id_number(kz_doc:doc()) -> kz_doc:doc(). +maybe_normalize_emergency_caller_id_number(Doc) -> + case kz_json:get_ne_binary_value(?KEY_EMERGENCY_NUMBER, Doc) of + 'undefined' -> Doc; + Number -> + NormalizedNumber = knm_converters:normalize(Number), + lager:debug("normalizing emergency caller id from ~s to ~s", [Number, NormalizedNumber]), + kz_json:set_value(?KEY_EMERGENCY_NUMBER, NormalizedNumber, Doc) + end. + +%%------------------------------------------------------------------------------ +%% @doc Generate MD5 amd SHA1 combination from a username and password +%% @end +%%------------------------------------------------------------------------------ +-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}. + +%%------------------------------------------------------------------------------ +%% @doc Validate a doc against a defined schema. +%% `OnSuccess' function will only be called if the Doc passes schema validation. +%% @end +%%------------------------------------------------------------------------------ +-spec validate_schema(kz_term:ne_binary() | kzd_schema:doc(), kazoo_documents:doc_validation_acc() + ,kazoo_documents:doc_validation_after_fun()) -> kazoo_documents:doc_validation_acc(). +validate_schema(<>, {Doc, ValidationErrors}, OnSuccess) -> + lager:debug("validating payload against schema ~s", [Schema]), + SchemaRequired = ?SHOULD_ENSURE_SCHEMA_IS_VALID, + + case kz_json_schema:load(Schema) of + {'ok', SchemaJObj} -> validate_schema(SchemaJObj, {Doc, ValidationErrors}, OnSuccess); + {'error', 'not_found'} when SchemaRequired -> + lager:error("~s schema not found and is required", [Schema]), + throw({'system_error', <<"schema '", Schema/binary, "' not found.">>}); + {'error', 'not_found'} -> + lager:error("~s schema not found, assuming schema validation passed, continuing anyway", [Schema]), + validate_schema_passed({Doc, ValidationErrors}, OnSuccess) + end; +validate_schema(SchemaJObj, {Doc, ValidationErrors}, OnSuccess) -> + Strict = ?SHOULD_FAIL_ON_INVALID_DATA, + SystemSL = ?CROSSBAR_STABILITY_LEVEL, + Options = [{'extra_validator_options', [{'stability_level', SystemSL}]}], + + try kz_json_schema:validate(SchemaJObj, kz_doc:public_fields(Doc), Options) of + {'ok', JObj} -> + lager:debug("schema validation passed"), + validate_schema_passed({JObj, ValidationErrors}, OnSuccess); + {'error', SchemaErrors} when Strict -> + lager:debug("schema validation errors when strictly validating"), + validate_schema_failed({Doc, ValidationErrors}, SchemaErrors); + {'error', SchemaErrors} -> + lager:debug("schema validation errors but not strictly validating, trying to fix request"), + maybe_fix_js_types({Doc, ValidationErrors}, SchemaErrors, SchemaJObj, OnSuccess) + catch + ?STACKTRACE('error', 'function_clause', ST) + lager:error("function clause failure"), + kz_util:log_stacktrace(ST), + throw({'system_error', <<"schema validation failed to run on the server">>}) + end. + +%%------------------------------------------------------------------------------ +%% @doc Validate a doc against a defined schema. +%% @end +%%------------------------------------------------------------------------------ +-spec validate_schema_passed(kazoo_documents:doc_validation_acc(), kazoo_documents:doc_validation_after_fun()) -> + kazoo_documents:doc_validation_acc(). +validate_schema_passed(ValidateAcc, OnSuccess) -> + case is_function(OnSuccess, 1) of + 'true' -> OnSuccess(ValidateAcc); + 'false' -> ValidateAcc + end. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_fix_js_types(kazoo_documents:doc_validation_acc(), [jesse_error:error_reason()], kzd_schema:doc() + ,kazoo_documents:doc_validation_after_fun()) -> kazoo_documents:doc_validation_acc(). +maybe_fix_js_types({Doc, ValidationErrors}, SchemaErrors, SchemaJObj, OnSuccess) -> + case kz_json_schema:fix_js_types(Doc, SchemaErrors) of + 'false' -> validate_schema_failed({Doc, ValidationErrors}, SchemaErrors); + {'true', NewDoc} -> + validate_schema(SchemaJObj, {NewDoc, ValidationErrors}, OnSuccess) + end. + +%%------------------------------------------------------------------------------ +%% @doc Add schema errors to doc validation errors. +%% @end +%%------------------------------------------------------------------------------ +-spec validate_schema_failed(kazoo_documents:doc_validation_acc(), [jesse_error:error_reason()]) -> kazoo_documents:doc_validation_acc(). +validate_schema_failed({Doc, ValidationErrors}, SchemaErrors) -> + {Doc + ,[schema_error_to_doc_validation_error(Error) || Error <- SchemaErrors] ++ ValidationErrors + }. + +%%------------------------------------------------------------------------------ +%% @doc Format a schema error into a doc validation error. +%% @end +%%------------------------------------------------------------------------------ +-spec schema_error_to_doc_validation_error(jesse_error:error_reason()) -> kazoo_documents:doc_validation_error(). +schema_error_to_doc_validation_error(Error) -> + {_ErrorCode, _ErrorMessage, ErrorJObj} = kz_json_schema:error_to_jobj(Error), + [Key] = kz_json:get_keys(ErrorJObj), + {[JObj], [Code]} = kz_json:get_values(Key, ErrorJObj), + lager:debug("schema validation failied ~s ~s: ~p", [Key, Code, JObj]), + {Key, Code, JObj}. diff --git a/core/kazoo_documents/src/kzd_schema.erl b/core/kazoo_documents/src/kzd_schema.erl index 41abf713ab7..df417920116 100644 --- a/core/kazoo_documents/src/kzd_schema.erl +++ b/core/kazoo_documents/src/kzd_schema.erl @@ -13,6 +13,10 @@ -include("kz_documents.hrl"). +-type doc() :: kz_json:object(). +-type docs() :: [doc()]. +-export_type([doc/0, docs/0]). + -define(SCHEMA_KEYWORDS_MAXLENGTH, <<"maxLength">>). %%% Load schema diff --git a/core/kazoo_documents/src/kzd_users.erl b/core/kazoo_documents/src/kzd_users.erl index 8dc50360716..287c2eb64e5 100644 --- a/core/kazoo_documents/src/kzd_users.erl +++ b/core/kazoo_documents/src/kzd_users.erl @@ -62,17 +62,17 @@ -export([voicemail_notify_callback/1, voicemail_notify_callback/2, set_voicemail_notify_callback/2]). --export([fetch/2]). --export([to_vcard/1]). --export([enable/1, disable/1]). --export([type/0]). --export([fax_settings/1]). --export([name/1]). --export([is_account_admin/1, is_account_admin/2]). --export([classifier_restriction/2, classifier_restriction/3, set_classifier_restriction/3]). --export([full_name/1, full_name/2]). - --export([full_name/3]). +-export([fetch/2 + ,to_vcard/1 + ,enable/1, disable/1 + ,type/0 + ,fax_settings/1 + ,name/1 + ,is_account_admin/1, is_account_admin/2 + ,classifier_restriction/2, classifier_restriction/3, set_classifier_restriction/3 + ,full_name/1, full_name/2, full_name/3 + ,validate/3 + ]). -include("kz_documents.hrl"). @@ -81,6 +81,19 @@ -export_type([doc/0, docs/0]). -define(SCHEMA, <<"users">>). +-define(LIST_BY_USERNAME, <<"users/list_by_username">>). +-define(LIST_BY_HOTDESK_ID, <<"users/list_by_hotdesk_id">>). + +-define(SYSCONFIG_CB_USERS, <<"crossbar.users">>). +-define(SHOULD_GENERATE_USER_PASSWORD_IF_EMPTY + ,kapps_config:get_is_true(?SYSCONFIG_CB_USERS, <<"generate_password_if_empty">>, 'false') + ). +-define(SHOULD_GENERATE_USER_USERNAME_IF_EMPTY + ,kapps_config:get_is_true(?SYSCONFIG_CB_USERS, <<"generate_username_if_empty">>, 'true') + ). +-define(SHOULD_RESET_IDENTITY_SECRET_ON_REHASH + ,kapps_config:get_is_true(?SYSCONFIG_CB_USERS, <<"reset_identity_secret_on_rehash">>, 'true') + ). -spec new() -> doc(). new() -> @@ -982,3 +995,389 @@ full_name(?NE_BINARY = First, _, _) -> <>; full_name(_, _, Default) -> Default. + +%%------------------------------------------------------------------------------ +%% @doc Validate a requested user can be created +%% +%% Returns the updated user doc (with relevant defaults) +%% or returns the validation error {Path, ErrorType, ErrorMessage} +%% @end +%%------------------------------------------------------------------------------ +-spec validate(kz_term:api_ne_binary(), kz_term:api_ne_binary(), doc()) -> + {'true', doc()} | + {'validation_errors', kazoo_documents:doc_validation_errors()} | + {'system_error', atom()}. +validate(AccountId, UserId, ReqJObj) -> + ValidateFuns = [fun maybe_normalize_username/3 + ,fun maybe_validate_username_is_unique/3 + ,fun maybe_normalize_emergency_caller_id_number/3 + ,fun maybe_import_credentials/3 + %% check_user_schema will load and merge the current doc's pvt fields + ,fun validate_user_schema/3 + %% this check must have the current doc + ,fun maybe_set_identity_secret/3 + %% this check must have the current doc + ,fun maybe_validate_hotdesk_id_is_unique/3 + %% this check must have the current doc + ,fun maybe_rehash_creds/3 + ], + try do_validation(AccountId, UserId, ReqJObj, ValidateFuns) of + {UserDoc, []} -> {'true', UserDoc}; + {_UserDoc, ValidationErrors} -> {'validation_errors', ValidationErrors} + catch + 'throw':SystemError -> SystemError + end. + +-spec do_validation(kz_term:api_ne_binary(), kz_term:api_ne_binary(), doc(), [kazoo_documents:doc_validation_fun()]) -> + {'true', doc()} | + {'validation_errors', kazoo_documents:doc_validation_errors()}. +do_validation(AccountId, UserId, ReqJObj, ValidateFuns) -> + lists:foldl(fun(F, Acc) -> F(AccountId, UserId, Acc) end + ,{ReqJObj, []} + ,ValidateFuns + ). + +%%------------------------------------------------------------------------------ +%% @doc If set, Normalize the user's username by converting it to lower case. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_normalize_username(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +maybe_normalize_username(_AccountId, _UserId, {Doc, Errors}) -> + case username(Doc) of + 'undefined' -> {Doc, Errors}; + Username -> + NormalizedUsername = kz_term:to_lower_binary(Username), + lager:debug("normalized username '~s' to '~s'", [Username, NormalizedUsername]), + {set_username(Doc, NormalizedUsername), Errors} + end. + +%%------------------------------------------------------------------------------ +%% @doc If set, Validate the user's username is unique within the account. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_validate_username_is_unique(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +maybe_validate_username_is_unique(AccountId, UserId, {Doc, Errors}) -> + Username = username(Doc), + CurrentUsername = case fetch(AccountId, UserId) of + {'ok', CurrentDoc} -> username(CurrentDoc); + {'error', _R} -> 'undefined' + end, + + case kz_term:is_empty(Username) + orelse Username =:= CurrentUsername + orelse is_username_unique(AccountId, UserId, Username) + of + 'true' -> + lager:debug("username '~s' (currently '~s') is unique within account", [Username, CurrentUsername]), + {Doc, Errors}; + 'false' -> + lager:error("username '~s' (currently '~s') is not unique within account", [Username, CurrentUsername]), + Msg = kz_json:from_list( + [{<<"message">>, <<"Username must be unique within account">>} + ,{<<"cause">>, Username} + ]), + {Doc, [{[<<"username">>], <<"unique">>, Msg} | Errors]} + end. + +%%------------------------------------------------------------------------------ +%% @doc Return true if a user's username is unique within an account, else +%% return false. +%% @end +%%------------------------------------------------------------------------------ +-spec is_username_unique(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kz_term:api_ne_binary()) -> boolean(). +is_username_unique(AccountId, UserId, Username) -> + AccountDb = kz_util:format_account_id(AccountId, 'encoded'), + ViewOptions = [{'key', Username}], + case kz_datamgr:get_results(AccountDb, ?LIST_BY_USERNAME, ViewOptions) of + {'ok', []} -> 'true'; + {'ok', [JObj]} -> kz_doc:id(JObj) =:= UserId; + _Else -> 'false' + end. + +%%------------------------------------------------------------------------------ +%% @doc If set, Normalize the user's emergency caller id number. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_normalize_emergency_caller_id_number(kz_term:api_ne_binary(), kz_term:api_ne_binary() + ,kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). +maybe_normalize_emergency_caller_id_number(_AccountId, _UserId, {Doc, Errors}) -> + {kzd_module_utils:maybe_normalize_emergency_caller_id_number(Doc), Errors}. + +%%------------------------------------------------------------------------------ +%% @doc Import user credentials if `<<"credentials">>' key is set. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_import_credentials(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +maybe_import_credentials(_AccountId, _UserId, {Doc, Errors}) -> + case kz_json:get_ne_value(<<"credentials">>, Doc) of + 'undefined' -> {Doc, Errors}; + Creds -> + lager:debug("importing user credentials"), + RemoveKeys = [<<"credentials">>, <<"pvt_sha1_auth">>], + kz_json:set_value(<<"pvt_md5_auth">> + ,Creds + ,kz_json:delete_keys(RemoveKeys, Doc) + ) + end. + +%%------------------------------------------------------------------------------ +%% @doc Verify the user doc against the user doc schema. +%% On Success merge the private fields from the current user doc into Doc. +%% @end +%%------------------------------------------------------------------------------ +-spec validate_user_schema(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +validate_user_schema(AccountId, UserId, {Doc, Errors}) -> + OnSuccess = fun(ValidateAcc) -> on_successful_schema_validation(AccountId, UserId, ValidateAcc) end, + kzd_module_utils:validate_schema(<<"users">>, {Doc, Errors}, OnSuccess). + +%%------------------------------------------------------------------------------ +%% @doc Executed after `validate_user_schema/3' if it passes schema validation. +%% If the UserId is defined then merge the current user doc's private fields +%% into Doc. +%% @end +%%------------------------------------------------------------------------------ +-spec on_successful_schema_validation(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +on_successful_schema_validation(AccountId, 'undefined', {Doc, Errors}) -> + lager:debug("new user doc passed schema validation"), + Props = [{<<"pvt_type">>, kzd_user:type()}], + JObj = kz_json:set_values(Props, Doc), + maybe_import_credentials(AccountId, 'undefined', {JObj, Errors}); +on_successful_schema_validation(AccountId, UserId, {Doc, Errors}) -> + lager:debug("updated user doc passed schema validation"), + UpdatedDoc = maybe_merge_current_private_fields(AccountId, UserId, Doc), + maybe_import_credentials(AccountId, UserId, {UpdatedDoc, Errors}). + +%%------------------------------------------------------------------------------ +%% @doc Merge the current (cached) doc's private fields into Doc. If the current +%% doc can not be found by Account Id and User Id, then return the unaltered +%% Doc. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_merge_current_private_fields(kz_term:ne_binary(), kz_term:ne_binary(), doc()) -> doc(). +maybe_merge_current_private_fields(AccountId, UserId, Doc) -> + case fetch(AccountId, UserId) of + {'ok', CurrentDoc} -> + kz_json:merge_jobjs(kz_doc:private_fields(CurrentDoc), Doc); + {'error', _R} -> Doc + end. + +%%------------------------------------------------------------------------------ +%% @doc Check if user has a non-empty `pvt_signature_secret' +%% If set then update `pvt_signature_secret'. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_set_identity_secret(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +maybe_set_identity_secret(_AccountId, _UserId, {Doc, Errors}) -> + case kz_auth_identity:has_doc_secret(Doc) of + 'true' -> {Doc, Errors}; + 'false' -> + lager:debug("initializing identity secret"), + {kz_auth_identity:reset_doc_secret(Doc), Errors} + end. + +%%------------------------------------------------------------------------------ +%% @doc If set, Validate the user's hotdesk id is unique within the account. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_validate_hotdesk_id_is_unique(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +maybe_validate_hotdesk_id_is_unique(AccountId, UserId, {Doc, Errors}) -> + HotdeskId = hotdesk_id(Doc), + CurrentHotdeskId = case fetch(AccountId, UserId) of + {'ok', CurrentDoc} -> hotdesk_id(CurrentDoc); + {'error', _R} -> 'undefined' + end, + + case kz_term:is_empty(HotdeskId) + orelse HotdeskId =:= CurrentHotdeskId + orelse is_hotdesk_id_unique(AccountId, UserId, HotdeskId) + of + 'true' -> + lager:debug("hotdesk id '~s' (currently '~s') is unique within account", [HotdeskId, CurrentHotdeskId]), + {Doc, Errors}; + 'false' -> + lager:error("hotdesk id '~s' (currently '~s') is not unique within account", [HotdeskId, CurrentHotdeskId]), + Msg = kz_json:from_list( + [{<<"message">>, <<"Hotdesk ID must be unique within account">>} + ,{<<"cause">>, HotdeskId} + ]), + {Doc, [{[<<"hotdesk">>, <<"id">>], <<"unique">>, Msg} | Errors]} + end. + +%%------------------------------------------------------------------------------ +%% @doc Return true if a user's hotdesk id is unique within an account, else +%% return false. +%% @end +%%------------------------------------------------------------------------------ +-spec is_hotdesk_id_unique(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> boolean(). +is_hotdesk_id_unique(AccountId, UserId, HotdeskId) -> + AccountDb = kz_util:format_account_id(AccountId, 'encoded'), + ViewOptions = [{'key', HotdeskId}], + case kz_datamgr:get_results(AccountDb, ?LIST_BY_HOTDESK_ID, ViewOptions) of + {'ok', []} -> 'true'; + {'ok', [JObj]} -> kz_doc:id(JObj) =:= UserId; + _Else -> 'false' + end. + +%%------------------------------------------------------------------------------ +%% @doc Maybe rehash the user's creds if needed. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_rehash_creds(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +maybe_rehash_creds(AccountId, UserId, {Doc, _Errors}=ValidateAcc) -> + Username = username(Doc), + Password = password(Doc), + CurrentDoc = case fetch(AccountId, UserId) of + {'ok', JObj} -> JObj; + {'error', _R} -> kz_json:new() + end, + CurrentUsername = username(CurrentDoc), + GeneratePassword = ?SHOULD_GENERATE_USER_PASSWORD_IF_EMPTY, + GenerateUsername = ?SHOULD_GENERATE_USER_USERNAME_IF_EMPTY, + GenerateCreds = + GenerateUsername + andalso GeneratePassword, + case + {Username =:= CurrentUsername + ,kz_term:is_empty(Username) + ,kz_term:is_empty(Password)} + of + {'false', 'false', 'false'} -> + lager:debug("requested different username (new: ~s current: ~s) with a password" + ,[Username, CurrentUsername] + ), + rehash_creds(Username, Password, ValidateAcc); + {'false', 'false', 'true'} -> + lager:debug("requested different username (new: ~s current: ~s) without a password" + ,[Username, CurrentUsername] + ), + maybe_generate_password_hash(GeneratePassword, Username, ValidateAcc); + {'false', 'true', 'false'} -> + lager:debug("requested no username but provided a password"), + maybe_generated_username_hash(GenerateUsername, Password, ValidateAcc); + {'false', 'true', 'true'} -> + lager:debug("requested no username or password"), + maybe_generate_creds_hash(GenerateCreds, ValidateAcc); + {'true', 'false', 'false'} -> + lager:debug("requested same username (new: ~s current: ~s) with a password" + ,[Username, CurrentUsername] + ), + rehash_creds(Username, Password, ValidateAcc); + {'true', 'false', 'true'} -> + lager:debug("requested same username (new: ~s current: ~s) without a password" + ,[Username, CurrentUsername] + ), + ValidateAcc; + {'true', 'true', 'false'} -> + lager:debug("requested no username (new: ~s current: ~s) with a password" + ,[Username, CurrentUsername] + ), + maybe_generated_username_hash(GenerateUsername, Password, ValidateAcc); + {'true', 'true', 'true'} -> + lager:debug("requested no username, no current username, and no password"), + maybe_generate_creds_hash(GenerateCreds, ValidateAcc) + end. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_generate_password_hash(boolean(), kz_term:ne_binary(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). +maybe_generate_password_hash('false', _Username, {Doc, Errors}) -> + Msg = kz_json:from_list( + [{<<"message">>, <<"The password must be provided when creating / updating the user name">>} + ,{<<"cause">>, username(Doc)} + ]), + {Doc, [{[<<"password">>], <<"required">>, Msg} | Errors]}; +maybe_generate_password_hash('true', Username, ValidateAcc) -> + rehash_creds(Username, generate_password(), ValidateAcc). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_generated_username_hash(boolean(), kz_term:ne_binary(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). +maybe_generated_username_hash('false', _Password, {Doc, Errors}) -> + Msg = kz_json:from_list( + [{<<"message">>, <<"The username must be provided when updating the password">>} + ,{<<"cause">>, username(Doc)} + ]), + {Doc, [{[<<"username">>], <<"required">>, Msg} | Errors]}; +maybe_generated_username_hash('true', Password, {Doc, Errors}) -> + Username = generate_username(), + UpdatedDoc = set_username(Doc, Username), + rehash_creds(Username, Password, {UpdatedDoc, Errors}). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_generate_creds_hash(boolean(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). +maybe_generate_creds_hash('false', {Doc, Errors}) -> + {remove_creds(Doc), Errors}; +maybe_generate_creds_hash('true', {Doc, Errors}) -> + Username = generate_username(), + UpdatedDoc = set_username(Doc, Username), + rehash_creds(Username, generate_password(), {UpdatedDoc, Errors}). + +%%------------------------------------------------------------------------------ +%% @doc Generate a random username beginning with `user_' and followed by 8 +%% random characters. +%% @end +%%------------------------------------------------------------------------------ +-spec generate_username() -> kz_term:ne_binary(). +generate_username() -> + lager:debug("generating random username"), + <<"user_", (kz_binary:rand_hex(8))/binary>>. + +%%------------------------------------------------------------------------------ +%% @doc Generate a random 32 char long password. +%% @end +%%------------------------------------------------------------------------------ +-spec generate_password() -> kz_term:ne_binary(). +generate_password() -> + lager:debug("generating random password"), + kz_binary:rand_hex(32). + +%%------------------------------------------------------------------------------ +%% @doc Remove `pvt_md5_auth' and `pvt_sha1_auth' from a user's doc. +%% @end +%%------------------------------------------------------------------------------ +-spec remove_creds(doc()) -> doc(). +remove_creds(Doc) -> + lager:debug("removing user creds"), + HashKeys = [<<"pvt_md5_auth">>, <<"pvt_sha1_auth">>], + kz_json:delete_keys(HashKeys, Doc). + +%%------------------------------------------------------------------------------ +%% @doc Rehash the users creds +%% @end +%%------------------------------------------------------------------------------ +-spec rehash_creds(kz_term:ne_binary(), kz_term:ne_binary(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). +rehash_creds(Username, Password, {Doc, Errors}) -> + lager:debug("updating cred hashes for user ~s", [Username]), + DocMD5 = kz_json:get_ne_value(<<"pvt_md5_auth">>, Doc), + DocSHA1 = kz_json:get_ne_value(<<"pvt_sha1_auth">>, Doc), + {MD5, SHA1} = kzd_module_utils:pass_hashes(Username, Password), + UpdatedDoc = kz_json:set_values([{<<"pvt_md5_auth">>, MD5} + ,{<<"pvt_sha1_auth">>, SHA1} + ] + ,Doc + ), + case ?SHOULD_RESET_IDENTITY_SECRET_ON_REHASH + andalso (DocMD5 =/= MD5 + orelse DocSHA1 =/= SHA1) + of + 'false' -> + {kz_json:delete_key(<<"password">>, UpdatedDoc), Errors}; + 'true' -> + lager:debug("resetting identity secret"), + {kz_auth_identity:reset_doc_secret(kz_json:delete_key(<<"password">>, UpdatedDoc)), Errors} + end. From b9a44c440d52e3aed021c47cde3cacc0629f9b3a Mon Sep 17 00:00:00 2001 From: Daniel Finke Date: Thu, 28 May 2020 22:21:01 +0000 Subject: [PATCH 2/3] PISTON-1078: do not automatically update accounts doc when validated - the bug allows the doc to be saved even if there are errors - it also causes there to be 2 saves per crossbar save --- core/kazoo_documents/src/kzd_accounts.erl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/kazoo_documents/src/kzd_accounts.erl b/core/kazoo_documents/src/kzd_accounts.erl index adbdaaee782..20005b954c5 100644 --- a/core/kazoo_documents/src/kzd_accounts.erl +++ b/core/kazoo_documents/src/kzd_accounts.erl @@ -1533,11 +1533,7 @@ validate_schema(ParentId, AccountId, {Doc, Errors}) -> on_successful_schema_validation(ParentId, 'undefined', {Doc, Errors}) -> lager:info("schema validation passed for new account: ~s", [kz_json:encode(Doc)]), {set_private_properties(ParentId, Doc), Errors}; -on_successful_schema_validation(_ParentId, AccountId, {Doc, Errors}) -> - case update(AccountId, kz_json:to_proplist(kz_json:flatten(Doc))) of - {'ok', UpdatedAccount} -> {UpdatedAccount, Errors}; - {'error', _E} -> {Doc, Errors} - end. +on_successful_schema_validation(_ParentId, _AccountId, ValidateAcc) -> ValidateAcc. -spec normalize_alphanum_name(kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). normalize_alphanum_name(_AccountId, {Doc, Errors}) -> From a091557c4cb21010daafbe88be23efa1f8265716 Mon Sep 17 00:00:00 2001 From: Ben Bradford Date: Tue, 2 Jun 2020 16:31:01 -0700 Subject: [PATCH 3/3] Define a common type for the kazoo document validation response and add the ability to pass the system_error massage in the response --- applications/crossbar/src/modules/cb_accounts.erl | 8 ++++++-- applications/crossbar/src/modules_v1/cb_users_v1.erl | 7 +++++-- applications/crossbar/src/modules_v2/cb_users_v2.erl | 7 +++++-- core/kazoo_documents/include/kazoo_documents.hrl | 4 ++++ core/kazoo_documents/src/kzd_accounts.erl | 5 +---- core/kazoo_documents/src/kzd_users.erl | 5 +---- 6 files changed, 22 insertions(+), 14 deletions(-) diff --git a/applications/crossbar/src/modules/cb_accounts.erl b/applications/crossbar/src/modules/cb_accounts.erl index d53d78b9a10..385bcdc4a77 100644 --- a/applications/crossbar/src/modules/cb_accounts.erl +++ b/applications/crossbar/src/modules/cb_accounts.erl @@ -494,8 +494,12 @@ validate_request(AccountId, Context) -> extra_validation(AccountId, Context1); {'validation_errors', ValidationErrors} -> cb_context:add_doc_validation_errors(Context, ValidationErrors); - {'system_error', Error} -> - cb_context:add_system_error(Error, Context) + {'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 get_parent_id_from_req(cb_context:context()) -> kz_term:api_ne_binary(). diff --git a/applications/crossbar/src/modules_v1/cb_users_v1.erl b/applications/crossbar/src/modules_v1/cb_users_v1.erl index 74d4650bc90..67462a8b39e 100644 --- a/applications/crossbar/src/modules_v1/cb_users_v1.erl +++ b/applications/crossbar/src/modules_v1/cb_users_v1.erl @@ -527,9 +527,12 @@ validate_request(UserId, Context) -> {'validation_errors', ValidationErrors} -> lager:info("validation errors on user"), cb_context:add_doc_validation_errors(Context, ValidationErrors); - {'system_error', Error} -> + {'system_error', Error} when is_atom(Error) -> lager:info("system error validating user: ~p", [Error]), - cb_context:add_system_error(Error, Context) + 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. %%------------------------------------------------------------------------------ diff --git a/applications/crossbar/src/modules_v2/cb_users_v2.erl b/applications/crossbar/src/modules_v2/cb_users_v2.erl index e7c12a91740..e1f666f9831 100644 --- a/applications/crossbar/src/modules_v2/cb_users_v2.erl +++ b/applications/crossbar/src/modules_v2/cb_users_v2.erl @@ -484,9 +484,12 @@ validate_request(UserId, Context) -> {'validation_errors', ValidationErrors} -> lager:info("validation errors on user"), cb_context:add_doc_validation_errors(Context, ValidationErrors); - {'system_error', Error} -> + {'system_error', Error} when is_atom(Error) -> lager:info("system error validating user: ~p", [Error]), - cb_context:add_system_error(Error, Context) + 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. %%------------------------------------------------------------------------------ diff --git a/core/kazoo_documents/include/kazoo_documents.hrl b/core/kazoo_documents/include/kazoo_documents.hrl index b7e28344045..2fd59f335cd 100644 --- a/core/kazoo_documents/include/kazoo_documents.hrl +++ b/core/kazoo_documents/include/kazoo_documents.hrl @@ -24,8 +24,12 @@ -type doc_validation_acc() :: {kz_doc:doc(), doc_validation_errors()}. -type doc_validation_fun() :: fun((kz_term:api_ne_binary(), doc_validation_acc()) -> doc_validation_acc()). -type doc_validation_after_fun() :: fun((doc_validation_acc()) -> doc_validation_acc()) | 'undefined'. +-type doc_validation_return() :: {'true', kz_doc:doc()} | + {'validation_errors', doc_validation_errors()} | + {'system_error', atom() | {atom(), kz_term:ne_binary()}}. -export_type([doc_validation_error/0, doc_validation_errors/0 ,doc_validation_acc/0, doc_validation_fun/0 ,doc_validation_after_fun/0 + ,doc_validation_return/0 ]). -endif. diff --git a/core/kazoo_documents/src/kzd_accounts.erl b/core/kazoo_documents/src/kzd_accounts.erl index 20005b954c5..587c3079f7e 100644 --- a/core/kazoo_documents/src/kzd_accounts.erl +++ b/core/kazoo_documents/src/kzd_accounts.erl @@ -1367,10 +1367,7 @@ is_alphanumeric(_) -> %% or returns the validation error {Path, ErrorType, ErrorMessage} %% @end %%------------------------------------------------------------------------------ --spec validate(kz_term:api_ne_binary(), kz_term:api_ne_binary(), doc()) -> - {'true', doc()} | - {'validation_errors', kazoo_documents:doc_validation_errors()} | - {'system_error', atom()}. +-spec validate(kz_term:api_ne_binary(), kz_term:api_ne_binary(), doc()) -> kazoo_documents:doc_validation_return(). validate(ParentId, AccountId, ReqJObj) -> ValidateFuns = [fun ensure_account_has_realm/2 ,fun ensure_account_has_timezone/2 diff --git a/core/kazoo_documents/src/kzd_users.erl b/core/kazoo_documents/src/kzd_users.erl index 287c2eb64e5..de6bc5766bf 100644 --- a/core/kazoo_documents/src/kzd_users.erl +++ b/core/kazoo_documents/src/kzd_users.erl @@ -1003,10 +1003,7 @@ full_name(_, _, Default) -> %% or returns the validation error {Path, ErrorType, ErrorMessage} %% @end %%------------------------------------------------------------------------------ --spec validate(kz_term:api_ne_binary(), kz_term:api_ne_binary(), doc()) -> - {'true', doc()} | - {'validation_errors', kazoo_documents:doc_validation_errors()} | - {'system_error', atom()}. +-spec validate(kz_term:api_ne_binary(), kz_term:api_ne_binary(), doc()) -> kazoo_documents:doc_validation_return(). validate(AccountId, UserId, ReqJObj) -> ValidateFuns = [fun maybe_normalize_username/3 ,fun maybe_validate_username_is_unique/3