diff --git a/applications/crossbar/src/modules/cb_callflows.erl b/applications/crossbar/src/modules/cb_callflows.erl index ec551a89ee4..c05ca71400b 100644 --- a/applications/crossbar/src/modules/cb_callflows.erl +++ b/applications/crossbar/src/modules/cb_callflows.erl @@ -171,168 +171,38 @@ load_callflow(CallflowId, Context) -> _Status -> Context1 end. --spec request_numbers(cb_context:context()) -> kz_term:ne_binaries() | kz_json:json_term(). -request_numbers(Context) -> - kz_json:get_ne_value(<<"numbers">>, cb_context:req_data(Context), []). - --spec request_patterns(cb_context:context()) -> kz_term:ne_binaries() | kz_json:json_term(). -request_patterns(Context) -> - kz_json:get_ne_value(<<"patterns">>, cb_context:req_data(Context), []). - %%------------------------------------------------------------------------------ -%% @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(CallflowId, Context) -> - case request_numbers(Context) of - [] -> validate_patterns(CallflowId, Context); - OriginalNumbers - when is_list(OriginalNumbers) -> - validate_callflow_schema(CallflowId, normalize_numbers(Context, OriginalNumbers)); - OriginalNumbers -> - Msg = kz_json:from_list( - [{<<"message">>, <<"Value is not of type array">>} - ,{<<"cause">>, OriginalNumbers} - ]), - cb_context:add_validation_error(<<"numbers">>, <<"type">>, Msg, Context) + ReqJObj = cb_context:req_data(Context), + AccountId = cb_context:account_id(Context), + case kzd_callflows:validate(AccountId, CallflowId, ReqJObj) of + {'true', CallflowJObj} -> + lager:debug("successfully validated callflow object"), + cb_context:update_successfully_validated_request(Context, CallflowJObj); + {'validation_errors', ValidationErrors} -> + lager:error("validation errors on callflow"), + cb_context:add_doc_validation_errors(Context, ValidationErrors); + {'system_error', Error} when is_atom(Error) -> + lager:error("system error validating callflow: ~p", [Error]), + cb_context:add_system_error(Error, Context); + {'system_error', {Error, Message}} -> + lager:error("system error validating callflow: ~p, ~p", [Error, Message]), + cb_context:add_system_error(Error, Message, Context) end. --spec normalize_numbers(cb_context:context(), kz_term:ne_binaries()) -> cb_context:context(). -normalize_numbers(Context, Nums) -> - Normalized = knm_converters:normalize(Nums, cb_context:account_id(Context)), - NewReqData = kz_json:set_value(<<"numbers">>, Normalized, cb_context:req_data(Context)), - cb_context:set_req_data(Context, NewReqData). - %%------------------------------------------------------------------------------ -%% @doc +%% @doc Validate an update request. %% @end %%------------------------------------------------------------------------------ -spec validate_patch(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). validate_patch(CallflowId, Context) -> crossbar_doc:patch_and_validate(CallflowId, Context, fun validate_request/2). --spec validate_patterns(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). -validate_patterns(CallflowId, Context) -> - case request_patterns(Context) of - [] -> - Msg = kz_json:from_list( - [{<<"message">>, <<"Callflows must be assigned at least one number or pattern">>} - ]), - cb_context:add_validation_error(<<"numbers">>, <<"required">>, Msg, Context); - Patterns - when is_list(Patterns) -> - validate_callflow_schema(CallflowId, Context); - Patterns -> - Msg = kz_json:from_list( - [{<<"message">>, <<"Value is not of type array">>} - ,{<<"cause">>, Patterns} - ]), - cb_context:add_validation_error(<<"patterns">>, <<"type">>, Msg, Context) - end. - --spec validate_uniqueness(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). -validate_uniqueness(CallflowId, Context) -> - Setters = [{fun validate_unique_numbers/2, CallflowId} - ,{fun validate_unique_patterns/2, CallflowId} - ], - cb_context:setters(Context, Setters). - --spec validate_unique_numbers(cb_context:context(), kz_term:api_binary()) -> cb_context:context(). -validate_unique_numbers(Context, CallflowId) -> - validate_unique_numbers(Context, CallflowId, request_numbers(Context)). - --spec validate_unique_numbers(cb_context:context(), kz_term:api_binary(), kz_term:ne_binaries()) -> cb_context:context(). -validate_unique_numbers(Context, _CallflowId, []) -> Context; -validate_unique_numbers(Context, CallflowId, Numbers) -> - Options = [{'keys', Numbers}], - case kz_datamgr:get_results(cb_context:account_db(Context), ?CB_LIST_BY_NUMBER, Options) of - {'error', Error} -> - lager:debug("failed to load callflows from account: ~p", [Error]), - cb_context:add_system_error(Error, Context); - {'ok', JObjs} -> - validate_number_conflicts(Context, CallflowId, JObjs) - end. - --spec validate_number_conflicts(cb_context:context(), kz_term:api_binary(), kz_json:objects()) -> cb_context:context(). -validate_number_conflicts(Context, 'undefined', JObjs) -> - add_number_conflicts(Context, JObjs); -validate_number_conflicts(Context, CallflowId, JObjs) -> - add_number_conflicts(Context, filter_callflow_list(CallflowId, JObjs)). - --spec add_number_conflicts(cb_context:context(), kz_json:objects()) -> cb_context:context(). -add_number_conflicts(Context, []) -> Context; -add_number_conflicts(Context, [JObj | JObjs]) -> - add_number_conflicts(add_number_conflict(Context, JObj), JObjs). - --spec add_number_conflict(cb_context:context(), kz_json:object()) -> cb_context:context(). -add_number_conflict(Context, JObj) -> - Id = kz_doc:id(JObj), - Name = kz_json:get_ne_binary_value([<<"value">>, <<"name">>], JObj, <<>>), - Number = kz_json:get_value(<<"key">>, JObj), - Msg = kz_json:from_list( - [{<<"message">>, <<"Number ", Number/binary, " exists in callflow ", Id/binary, " ", Name/binary>>} - ,{<<"cause">>, Number} - ]), - cb_context:add_validation_error(<<"numbers">>, <<"unique">>, Msg, Context). - --spec validate_unique_patterns(cb_context:context(), kz_term:api_binary()) -> cb_context:context(). -validate_unique_patterns(Context, CallflowId) -> - validate_unique_patterns(Context, CallflowId, request_patterns(Context)). - --spec validate_unique_patterns(cb_context:context(), kz_term:api_binary(), kz_term:ne_binaries()) -> cb_context:context(). -validate_unique_patterns(Context, _CallflowId, []) -> Context; -validate_unique_patterns(Context, CallflowId, Patterns) -> - Options = [{'keys', Patterns}], - case kz_datamgr:get_results(cb_context:account_db(Context), ?CB_LIST_BY_PATTERN, Options) of - {'error', Error} -> - lager:debug("failed to load callflows from account: ~p", [Error]), - cb_context:add_system_error(Error, Context); - {'ok', JObjs} -> - validate_pattern_conflicts(Context, CallflowId, JObjs) - end. - --spec validate_pattern_conflicts(cb_context:context(), kz_term:api_binary(), kz_json:objects()) -> cb_context:context(). -validate_pattern_conflicts(Context, 'undefined', JObjs) -> - add_pattern_conflicts(Context, JObjs); -validate_pattern_conflicts(Context, CallflowId, JObjs) -> - add_pattern_conflicts(Context, filter_callflow_list(CallflowId, JObjs)). - --spec add_pattern_conflicts(cb_context:context(), kz_json:objects()) -> cb_context:context(). -add_pattern_conflicts(Context, []) -> Context; -add_pattern_conflicts(Context, [JObj | JObjs]) -> - add_pattern_conflicts(add_pattern_conflict(Context, JObj), JObjs). - --spec add_pattern_conflict(cb_context:context(), kz_json:object()) -> cb_context:context(). -add_pattern_conflict(Context, JObj) -> - Id = kz_doc:id(JObj), - Name = kz_json:get_ne_binary_value([<<"value">>, <<"name">>], JObj, <<>>), - Pattern = kz_json:get_value(<<"key">>, JObj), - Msg = kz_json:from_list( - [{<<"message">>, <<"Pattern ", Pattern/binary, " exists in callflow ", Id/binary, " ", Name/binary>>} - ,{<<"cause">>, Pattern} - ]), - cb_context:add_validation_error(<<"patterns">>, <<"unique">>, Msg, Context). - --spec validate_callflow_schema(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). -validate_callflow_schema(CallflowId, Context) -> - OnSuccess = fun(C) -> - C1 = validate_uniqueness(CallflowId, on_successful_validation(CallflowId, C)), - - Doc = cb_context:doc(C1), - Nums = kz_json:get_list_value(<<"numbers">>, Doc, []), - cb_modules_util:validate_number_ownership(Nums, C1) - end, - cb_context:validate_request_data(<<"callflows">>, Context, OnSuccess). - --spec on_successful_validation(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). -on_successful_validation('undefined', Context) -> - cb_context:set_doc(Context - ,kz_doc:set_type(cb_context:doc(Context), kzd_callflow:type()) - ); -on_successful_validation(CallflowId, Context) -> - crossbar_doc:load_merge(CallflowId, Context, ?TYPE_CHECK_OPTION(kzd_callflow:type())). - %%------------------------------------------------------------------------------ %% @doc Normalizes the results of a view. %% @end @@ -404,14 +274,6 @@ track_assignment('delete', Context) -> Updates = cb_modules_util:apply_assignment_updates(Unassigned, Context), cb_modules_util:log_assignment_updates(Updates). --spec filter_callflow_list(kz_term:api_binary(), kz_json:objects()) -> kz_json:objects(). -filter_callflow_list('undefined', JObjs) -> JObjs; -filter_callflow_list(CallflowId, JObjs) -> - [JObj - || JObj <- JObjs, - kz_doc:id(JObj) =/= CallflowId - ]. - %%------------------------------------------------------------------------------ %% @doc collect additional information about the objects referenced in the flow %% @end @@ -507,7 +369,7 @@ create_metadata(Doc) -> ] ). --spec metadata_builder(kz_json:path(), kz_json:object(), kz_json:object()) -> +-spec metadata_builder(kz_json:key(), kz_json:object(), kz_json:object()) -> kz_json:object(). metadata_builder(<<"name">> = Key, Doc, Metadata) -> case kz_doc:type(Doc) of @@ -519,7 +381,7 @@ metadata_builder(<<"name">> = Key, Doc, Metadata) -> metadata_builder(Key, Doc, Metadata) -> maybe_copy_value(Key, Doc, Metadata). --spec maybe_copy_value(kz_json:path(), kz_json:object(), kz_json:object()) -> +-spec maybe_copy_value(kz_json:key(), kz_json:object(), kz_json:object()) -> kz_json:object(). maybe_copy_value(Key, Doc, Metadata) -> case kz_json:get_value(Key, Doc) of diff --git a/applications/crossbar/src/modules/cb_modules_util.erl b/applications/crossbar/src/modules/cb_modules_util.erl index 116d0361283..69c500294d4 100644 --- a/applications/crossbar/src/modules/cb_modules_util.erl +++ b/applications/crossbar/src/modules/cb_modules_util.erl @@ -199,28 +199,18 @@ remove_plaintext_password(Context) -> -spec validate_number_ownership(kz_term:ne_binaries(), cb_context:context()) -> cb_context:context(). validate_number_ownership(Numbers, Context) -> - Options = [{'auth_by', cb_context:auth_account_id(Context)}], - #{ko := KOs} = knm_numbers:get(Numbers, Options), - case maps:fold(fun validate_number_ownership_fold/3, [], KOs) of - [] -> Context; - Unauthorized -> + AccountId = cb_context:auth_account_id(Context), + case kz_term:is_empty(AccountId) + orelse knm_number:validate_ownership(AccountId, Numbers) + of + 'true' -> Context; + {'false', Unauthorized} -> Prefix = <<"unauthorized to use ">>, NumbersStr = kz_binary:join(Unauthorized, <<", ">>), Message = <>, cb_context:add_system_error(403, 'forbidden', Message, Context) end. --spec validate_number_ownership_fold(knm_numbers:num(), knm_numbers:ko(), kz_term:ne_binaries()) -> - kz_term:ne_binaries(). -validate_number_ownership_fold(_, Reason, Unauthorized) when is_atom(Reason) -> - %% Ignoring atom reasons, i.e. 'not_found' or 'not_reconcilable' - Unauthorized; -validate_number_ownership_fold(Number, ReasonJObj, Unauthorized) -> - case knm_errors:error(ReasonJObj) of - <<"forbidden">> -> [Number|Unauthorized]; - _ -> Unauthorized - end. - -type assignment_to_apply() :: {kz_term:ne_binary(), kz_term:api_binary()}. -type assignments_to_apply() :: [assignment_to_apply()]. -type port_req_assignment() :: {kz_term:ne_binary(), kz_term:api_binary(), kz_json:object()}. diff --git a/core/kazoo_documents/src/kzd_callflows.erl b/core/kazoo_documents/src/kzd_callflows.erl index bdcb5053c08..6266bc56c8f 100644 --- a/core/kazoo_documents/src/kzd_callflows.erl +++ b/core/kazoo_documents/src/kzd_callflows.erl @@ -15,17 +15,29 @@ -export([numbers/1, numbers/2, set_numbers/2]). -export([patterns/1, patterns/2, set_patterns/2]). +-export([fetch/2 + ,type/0 + ,validate/3 + ]). -include("kz_documents.hrl"). -type doc() :: kz_json:object(). --export_type([doc/0]). +-type docs() :: [doc()]. +-export_type([doc/0 + ,docs/0 + ]). + +-define(KEY_NUMBERS, [<<"numbers">>]). +-define(KEY_PATTERNS, [<<"patterns">>]). -define(SCHEMA, <<"callflows">>). +-define(LIST_BY_NUMBER, <<"callflows/listing_by_number">>). +-define(LIST_BY_PATTERN, <<"callflows/listing_by_pattern">>). -spec new() -> doc(). new() -> - kz_json_schema:default_object(?SCHEMA). + kz_doc:set_type(kz_json_schema:default_object(?SCHEMA), type()). -spec featurecode(doc()) -> kz_term:api_object(). featurecode(Doc) -> @@ -105,11 +117,11 @@ numbers(Doc) -> -spec numbers(doc(), Default) -> kz_term:ne_binaries() | Default. numbers(Doc, Default) -> - kz_json:get_list_value([<<"numbers">>], Doc, Default). + kz_json:get_list_value(?KEY_NUMBERS, Doc, Default). -spec set_numbers(doc(), kz_term:ne_binaries()) -> doc(). set_numbers(Doc, Numbers) -> - kz_json:set_value([<<"numbers">>], Numbers, Doc). + kz_json:set_value(?KEY_NUMBERS, Numbers, Doc). -spec patterns(doc()) -> kz_term:ne_binaries(). patterns(Doc) -> @@ -117,8 +129,357 @@ patterns(Doc) -> -spec patterns(doc(), Default) -> kz_term:ne_binaries() | Default. patterns(Doc, Default) -> - kz_json:get_list_value([<<"patterns">>], Doc, Default). + kz_json:get_list_value(?KEY_PATTERNS, Doc, Default). -spec set_patterns(doc(), kz_term:ne_binaries()) -> doc(). set_patterns(Doc, Patterns) -> - kz_json:set_value([<<"patterns">>], Patterns, Doc). + kz_json:set_value(?KEY_PATTERNS, Patterns, Doc). + +%%------------------------------------------------------------------------------ +%% @doc Fetch a callflow from cache. +%% @end +%%------------------------------------------------------------------------------ +-spec fetch(kz_term:api_ne_binary(), kz_term:api_ne_binary()) -> + {'ok', doc()} | + kz_datamgr:data_error(). +fetch('undefined', _CallflowId) -> + {'error', 'invalid_db_name'}; +fetch(_Account, 'undefined') -> + {'error', 'not_found'}; +fetch(Account, CallflowId=?NE_BINARY) -> + AccountDb = kz_util:format_account_db(Account), + kz_datamgr:open_cache_doc(AccountDb, CallflowId). + +-spec type() -> kz_term:ne_binary(). +type() -> <<"callflow">>. + +%%------------------------------------------------------------------------------ +%% @doc Validate a requested callflow can be created +%% +%% Returns the updated callflow 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()) -> kazoo_documents:doc_validation_return(). +validate(AccountId, CallflowId, ReqJObj) -> + ValidateFuns = [fun validate_either_numbers_or_patterns_is_set/3 + ,fun maybe_validate_numbers/3 + ,fun maybe_validate_patterns/3 + ,fun validate_schema/3 + ], + try do_validation(AccountId, CallflowId, ReqJObj, ValidateFuns) of + {CallflowDoc, []} -> {'true', CallflowDoc}; + {_CallflowDoc, 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, CallflowId, ReqJObj, ValidateFuns) -> + lists:foldl(fun(F, Acc) -> F(AccountId, CallflowId, Acc) end + ,{ReqJObj, []} + ,ValidateFuns + ). + +%%------------------------------------------------------------------------------ +%% @doc Validate that either of the callflow's `numbers' or `patterns' attribute +%% is set and has at least one list element. +%% @end +%%------------------------------------------------------------------------------ +-spec validate_either_numbers_or_patterns_is_set(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +validate_either_numbers_or_patterns_is_set(_AccountId, _CallflowId, {Doc, Errors}=ValidateAcc) -> + case {numbers(Doc, []), patterns(Doc, [])} of + {[], []} -> + lager:error("callflows must be assigned at least one number or pattern"), + Msg = kz_json:from_list( + [{<<"message">>, <<"Callflows must be assigned at least one number or pattern">>} + ]), + {Doc, [{?KEY_NUMBERS, <<"required">>, Msg} | Errors]}; + {_Numbers, _Patterns} -> + ValidateAcc + end. + +%%------------------------------------------------------------------------------ +%% @doc If set, run all validation functions on the field `numbers'. +%% First verify the `numbers' value is a non empty list, If it is then do +%% further validation on the numbers list, if it is not then skip all further +%% number validation steps. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_validate_numbers(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +maybe_validate_numbers(AccountId, CallflowId, {Doc, _Errors}=ValidateAcc) -> + case kz_json:get_value(?KEY_NUMBERS, Doc, 'undefined') of + 'undefined' -> + lager:debug("callflow's ~p is not defined, skipping further number validation", [?KEY_NUMBERS]), + ValidateAcc; + [] -> + lager:debug("callflow's ~p is an empty list, skipping further number validation", [?KEY_NUMBERS]), + ValidateAcc; + Numbers when is_list(Numbers) -> + lager:debug("callflow's ~p is a non empty list, running further number validation", [?KEY_NUMBERS]), + do_further_number_validation(AccountId, CallflowId, ValidateAcc); + Numbers -> + %% No need to add error as this will be added by schema check + lager:error("validation error, callflow's ~p is not of type list, value: ~p, skipping further number validation" + ,[?KEY_NUMBERS, Numbers]), + ValidateAcc + end. + +%%------------------------------------------------------------------------------ +%% @doc Run all number validation functions on the list field `numbers'. +%% @end +%%------------------------------------------------------------------------------ +-spec do_further_number_validation(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +do_further_number_validation(AccountId, CallflowId, {_Doc, _Errors}=ValidateAcc) -> + NumValidateFuns = [fun normalize_numbers/3 + ,fun(AID, CID, Acc) -> maybe_validate_attribute_list_is_unique_within_account_callflows(?KEY_NUMBERS, AID, CID, Acc) end + ,fun validate_number_ownership/3 + ], + lists:foldl(fun(F, Acc) -> F(AccountId, CallflowId, Acc) end + ,ValidateAcc + ,NumValidateFuns + ). + +%%------------------------------------------------------------------------------ +%% @doc Normalize the numbers in the `numbers' list. +%% @end +%%------------------------------------------------------------------------------ +-spec normalize_numbers(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +normalize_numbers(AccountId, _CallflowId, {Doc, Errors}) -> + lager:debug("normalizing callflow's numbers"), + Normalized = knm_converters:normalize(numbers(Doc, []), AccountId), + {set_numbers(Doc, Normalized), Errors}. + +%%------------------------------------------------------------------------------ +%% @doc Validate that all numbers set are owned by the account. +%% @end +%%------------------------------------------------------------------------------ +-spec validate_number_ownership(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +validate_number_ownership(AccountId, _CallflowId, {Doc, _Errors}=ValidateAcc) -> + Numbers = numbers(Doc, []), + lager:debug("validating number ownership for numbers ~p", [Numbers]), + case knm_number:validate_ownership(AccountId, Numbers) of + 'true' -> ValidateAcc; + {'false', Unauthorized} -> + lager:error("numbers ~p are not owned by the account ~p", [Unauthorized, AccountId]), + Prefix = <<"unauthorized to use ">>, + NumbersStr = kz_binary:join(Unauthorized, <<", ">>), + Message = <>, + throw({'system_error', {'forbidden', Message}}) + end. + +%%------------------------------------------------------------------------------ +%% @doc If set, run all validation functions on the field `patterns'. +%% First verify the `patterns' value is a non empty list, If it is then do +%% further validation on the patterns list, if it is not then skip all further +%% pattern validation steps. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_validate_patterns(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +maybe_validate_patterns(AccountId, CallflowId, {Doc, _Errors}=ValidateAcc) -> + case kz_json:get_value(?KEY_PATTERNS, Doc, 'undefined') of + 'undefined' -> + lager:debug("callflow's ~p is not defined, skipping further pattern validation", [?KEY_PATTERNS]), + ValidateAcc; + [] -> + lager:debug("callflow's ~p is an empty list, skipping further pattern validation", [?KEY_PATTERNS]), + ValidateAcc; + Patterns when is_list(Patterns) -> + lager:debug("callflow's ~p is a non empty list, running further pattern validation", [?KEY_PATTERNS]), + do_further_pattern_validation(AccountId, CallflowId, ValidateAcc); + Patterns -> + %% No need to add error as this will be added by schema check + lager:error("validation error, callflow's ~p is not of type list, value: ~p, skipping further pattern validation" + ,[?KEY_PATTERNS, Patterns]), + ValidateAcc + end. + +%%------------------------------------------------------------------------------ +%% @doc Run all pattern validation functions on the list field `patterns'. +%% @end +%%------------------------------------------------------------------------------ +-spec do_further_pattern_validation(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +do_further_pattern_validation(AccountId, CallflowId, {_Doc, _Errors}=ValidateAcc) -> + ValidateFuns = [fun(AID, CID, Acc) -> maybe_validate_attribute_list_is_unique_within_account_callflows(?KEY_PATTERNS, AID, CID, Acc) end], + lists:foldl(fun(F, Acc) -> F(AccountId, CallflowId, Acc) end + ,ValidateAcc + ,ValidateFuns + ). + +%%------------------------------------------------------------------------------ +%% @doc Validate each of the callflow's `patterns' or `numbers' is unique within +%% the account's callflows. +%% First check if the patterns / numbers have changed from the current callflow +%% doc. +%% If they have not changed then skip any further checks, +%% If they have changed or its a new callflow then verify each of the patterns / +%% numbers is not used in any of the other callflows within the account. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_validate_attribute_list_is_unique_within_account_callflows(kz_json:get_key(), kz_term:api_ne_binary() + ,kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +maybe_validate_attribute_list_is_unique_within_account_callflows(Key, AccountId, CallflowId, {Doc, _Errors}=ValidateAcc) -> + NewValue = kz_json:get_list_value(Key, Doc, 'undefined'), + CurrentValue = case fetch(AccountId, CallflowId) of + {'ok', CurrentDoc} -> kz_json:get_list_value(Key, CurrentDoc, []); + {'error', _R} -> 'undefined' + end, + case kz_term:is_empty(NewValue) + orelse (not(kz_term:is_empty(CurrentValue)) + andalso length(NewValue) =:= length(CurrentValue) + andalso (NewValue -- CurrentValue) =:= []) + of + 'true' when NewValue =:= 'undefined' -> + lager:debug("~p is not set on callflow, skipping unique ~p checks", [Key, Key]), + ValidateAcc; + 'true' -> + lager:debug("~p '~p' (currently '~p') has not changed from the current callflow, skipping unique checks" + ,[Key, NewValue, CurrentValue]), + ValidateAcc; + 'false' when CurrentValue =:= 'undefined' -> + lager:debug("new callflow with ~p '~p', running unique checks", [Key, NewValue]), + do_validate_attribute_list_is_unique_within_account_callflows(Key, AccountId, CallflowId, ValidateAcc); + 'false' -> + lager:debug("~p '~p' (currently '~p') have changed from the current callflow, running unique checks" + ,[Key, NewValue, CurrentValue]), + do_validate_attribute_list_is_unique_within_account_callflows(Key, AccountId, CallflowId, ValidateAcc) + end. + +%%------------------------------------------------------------------------------ +%% @doc Check the callflow's `patterns' or `numbers' against the accounts +%% callflows in the db. +%% If any of the patterns / numbers are found in other callflows then add an +%% error to the `doc_validation_acc()' for each pattern conflict. +%% @end +%%------------------------------------------------------------------------------ +-spec do_validate_attribute_list_is_unique_within_account_callflows(kz_json:get_key(), kz_term:api_ne_binary(), kz_term:api_ne_binary() + ,kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). +do_validate_attribute_list_is_unique_within_account_callflows(Key, AccountId, CallflowId, {Doc, _Errors}=ValidateAcc) -> + AccountDb = kz_util:format_account_id(AccountId, 'encoded'), + Options = [{'keys', kz_json:get_list_value(Key, Doc, [])}], + View = get_view_by(Key), + case kz_datamgr:get_results(AccountDb, View, Options) of + {'ok', []} -> + ValidateAcc; + {'ok', CallflowViews} -> + validate_attribute_conflicts(Key, CallflowViews, CallflowId, ValidateAcc); + {'error', Error} -> + lager:error("failed to load view ~s from account ~s, error: ~p", [View, AccountDb, Error]), + throw({'system_error', Error}) + end. + +%%------------------------------------------------------------------------------ +%% @doc Get the DB view name for callflows listed by `numbers' or `patterns'. +%% @end +%%------------------------------------------------------------------------------ +-spec get_view_by(kz_json:get_key()) -> kz_term:ne_binary(). +get_view_by(?KEY_NUMBERS) -> ?LIST_BY_NUMBER; +get_view_by(?KEY_PATTERNS) -> ?LIST_BY_PATTERN. + +%%------------------------------------------------------------------------------ +%% @doc Validate the `numbers' or `patterns' conflicts are not part of its own +%% callflow. +%% If the conflict is part of its own callflow then ignore, else add the +%% conflict errors to the `doc_validation_acc()' +%% @end +%%------------------------------------------------------------------------------ +-spec validate_attribute_conflicts(kz_json:get_key(), kz_json:objects(), kz_term:api_ne_binary(), kazoo_documents:doc_validation_acc()) -> + kazoo_documents:doc_validation_acc(). +validate_attribute_conflicts(Key, CallflowViews, 'undefined', ValidateAcc) -> + add_attribute_conflict_errors(Key, CallflowViews, ValidateAcc); +validate_attribute_conflicts(Key, CallflowViews, CallflowId, ValidateAcc) -> + ConflictingViewObj = filter_callflow_list(CallflowId, CallflowViews), + add_attribute_conflict_errors(Key, ConflictingViewObj, ValidateAcc). + +%%------------------------------------------------------------------------------ +%% @doc Remove any callflows from a list of callflows where the callflow's id +%% value equals the supplied `CallflowId'. +%% @end +%%------------------------------------------------------------------------------ +-spec filter_callflow_list(kz_term:ne_binary(), kz_json:objects()) -> kz_json:objects(). +filter_callflow_list(CallflowId, Callflows) -> + [Callflow + || Callflow <- Callflows, + kz_doc:id(Callflow) =/= CallflowId + ]. + +%%------------------------------------------------------------------------------ +%% @doc Add all `numbers' or `patterns' conflict errors to the +%% `doc_validation_acc()'. +%% @end +%%------------------------------------------------------------------------------ +-spec add_attribute_conflict_errors(kz_json:get_key(), docs(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). +add_attribute_conflict_errors(_Key, [], ValidateAcc) -> ValidateAcc; +add_attribute_conflict_errors(Key, [CallflowView | CallflowViews], ValidateAcc) -> + UpdatedValidateAcc = add_attribute_conflict_error(Key, CallflowView, ValidateAcc), + add_attribute_conflict_errors(Key, CallflowViews, UpdatedValidateAcc). + +%%------------------------------------------------------------------------------ +%% @doc Add a `numbers' or `patterns' conflict error to the +%% `doc_validation_acc()'. +%% @end +%%------------------------------------------------------------------------------ +-spec add_attribute_conflict_error(kz_json:get_key(), doc(), kazoo_documents:doc_validation_acc()) -> kazoo_documents:doc_validation_acc(). +add_attribute_conflict_error(Key, CallflowView, {Doc, Errors}) -> + Id = kz_doc:id(CallflowView), + Name = kz_json:get_ne_binary_value([<<"value">>, <<"name">>], CallflowView, <<>>), + Number = kz_json:get_value(<<"key">>, CallflowView), + lager:error("validation error, ~p ~p exists in callflow ~s (~s)", [Key, Number, Id, Name]), + Msg = kz_json:from_list( + [{<<"message">>, <>} + ,{<<"cause">>, Number} + ]), + {Doc, [{Key, <<"unique">>, Msg} | Errors]}. + +%%------------------------------------------------------------------------------ +%% @doc Verify the doc against the callflow doc schema. +%% On Success merge the private fields from the current user doc into Doc. +%% @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(AccountId, CallflowId, {_Doc, _Errors}=ValidateAcc) -> + lager:debug("checking callflow doc against schema"), + OnSuccess = fun(ValAcc) -> on_successful_schema_validation(AccountId, CallflowId, ValAcc) end, + kzd_module_utils:validate_schema(<<"callflows">>, ValidateAcc, OnSuccess). + +%%------------------------------------------------------------------------------ +%% @doc Executed after `validate_schema/3' if it passes schema validation. +%% If the callflow Id is defined then merge the current callflow 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 callflow doc passed schema validation"), + {kz_doc:set_type(Doc, type()), Errors}; +on_successful_schema_validation(AccountId, UserId, {Doc, Errors}) -> + lager:debug("updated callflow doc passed schema validation"), + UpdatedDoc = maybe_merge_current_private_fields(AccountId, UserId, Doc), + {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, CallflowId, Doc) -> + case fetch(AccountId, CallflowId) of + {'ok', CurrentDoc} -> + kz_json:merge_jobjs(kz_doc:private_fields(CurrentDoc), Doc); + {'error', _R} -> Doc + end. diff --git a/core/kazoo_number_manager/src/knm_number.erl b/core/kazoo_number_manager/src/knm_number.erl index f01d1249b7e..ab434dc0a9f 100644 --- a/core/kazoo_number_manager/src/knm_number.erl +++ b/core/kazoo_number_manager/src/knm_number.erl @@ -31,6 +31,7 @@ ,ensure_can_create/1 ,ensure_can_load_to_create/1 ,state_for_create/1, allowed_creation_states/1, allowed_creation_states/2 + ,validate_ownership/2 ]). -ifdef(TEST). @@ -233,6 +234,35 @@ ensure_can_create(Num, Options) -> ensure_account_can_create(Options, knm_number_options:auth_by(Options)) andalso ensure_number_is_not_porting(Num, Options). +%%------------------------------------------------------------------------------ +%% @doc Return true if the numbers are all owned by the account, else return the +%% tuple where the first element is `false' and the second is a list of all the +%% numbers that are not owned by the account. +%% @end +%%------------------------------------------------------------------------------ +-spec validate_ownership(kz_term:ne_binary(), knm_numbers:num() | knm_numbers:nums()) -> 'true' | {'false', knm_numbers:nums()}. +validate_ownership(AccountId, Number=?NE_BINARY) -> + validate_ownership(AccountId, [Number]); +validate_ownership(_AccountId, []) -> 'true'; +validate_ownership(AccountId, Numbers) -> + Options = [{'auth_by', AccountId}], + #{'ko' := KOs} = knm_numbers:get(Numbers, Options), + case maps:fold(fun validate_ownership_fold/3, [], KOs) of + [] -> 'true'; + Unauthorized -> {'false', Unauthorized} + end. + +-spec validate_ownership_fold(knm_numbers:num(), knm_numbers:ko(), kz_term:ne_binaries()) -> + kz_term:ne_binaries(). +validate_ownership_fold(_, Reason, UnauthorizedAcc) when is_atom(Reason) -> + %% Ignoring atom reasons, i.e. 'not_found' or 'not_reconcilable' + UnauthorizedAcc; +validate_ownership_fold(Number, ReasonJObj, UnauthorizedAcc) -> + case knm_errors:error(ReasonJObj) of + <<"forbidden">> -> [Number|UnauthorizedAcc]; + _ -> UnauthorizedAcc + end. + -ifdef(TEST). -define(LOAD_ACCOUNT(Options, _AccountId) ,{'ok', props:get_value(<<"auth_by_account">>, Options)}