diff --git a/apps/capi/src/capi_auth.erl b/apps/capi/src/capi_auth.erl index 74f843c..8cf07ab 100644 --- a/apps/capi/src/capi_auth.erl +++ b/apps/capi/src/capi_auth.erl @@ -30,10 +30,7 @@ auth_data => tk_auth_data:auth_data() }}. --type resolution() :: - allowed - | forbidden - | {forbidden, _Reason}. +-type resolution() :: allowed | forbidden. -type consumer() :: capi_auth_legacy:consumer(). @@ -96,8 +93,11 @@ authorize_api_key(?unauthorized({TokenType, Token}), TokenContext, WoodyContext) ProcessingContext :: capi_handler:processing_context() ) -> resolution(). authorize_operation(Prototypes, ProcessingContext) -> - AuthContext = extract_auth_context(ProcessingContext), - do_authorize_operation(Prototypes, get_auth_data(AuthContext), ProcessingContext). + AuthData = get_auth_data(extract_auth_context(ProcessingContext)), + #{swagger_context := SwagContext, woody_context := WoodyContext} = ProcessingContext, + Fragments = capi_bouncer:gather_context_fragments(AuthData, SwagContext, WoodyContext), + Fragments1 = capi_bouncer_context:build(Prototypes, Fragments, WoodyContext), + capi_bouncer:judge(Fragments1, WoodyContext). %% @@ -155,8 +155,3 @@ make_context(AuthData, LegacyContext) -> legacy => LegacyContext, auth_data => AuthData }). - -do_authorize_operation(Prototypes, AuthData, #{swagger_context := SwagContext, woody_context := WoodyContext}) -> - Fragments = capi_bouncer:gather_context_fragments(AuthData, SwagContext, WoodyContext), - Fragments1 = capi_bouncer_context:build(Prototypes, Fragments, WoodyContext), - capi_bouncer:judge(Fragments1, WoodyContext). diff --git a/apps/capi/src/capi_handler.erl b/apps/capi/src/capi_handler.erl index 44bdc02..ce5e912 100644 --- a/apps/capi/src/capi_handler.erl +++ b/apps/capi/src/capi_handler.erl @@ -10,6 +10,7 @@ -export([handle_request/4]). -export([respond/1]). -export([respond_if_undefined/2]). +-export([respond_if_forbidden/2]). %% @@ -58,7 +59,6 @@ %% @WARNING Must be refactored in case of different classes of users using this API -define(REALM, <<"external">>). --define(DOMAIN, <<"common-api">>). -spec authorize_api_key(operation_id(), swag_server:api_key(), request_context(), handler_opts()) -> Result :: false | {true, capi_auth:preauth_context()}. @@ -153,10 +153,6 @@ handle_function_(OperationID, Req, SwagContext0, HandlerOpts) -> allowed -> Process(); forbidden -> - _ = logger:info("Authorization failed"), - {ok, {401, #{}, undefined}}; - {forbidden, Error} -> - _ = logger:info("Authorization failed due to ~p", [Error]), {ok, {401, #{}, undefined}} end catch @@ -204,6 +200,13 @@ respond_if_undefined(undefined, Response) -> respond_if_undefined(_, _Response) -> ok. +-spec respond_if_forbidden(Resolution, response()) -> Resolution | throw(response()) when + Resolution :: capi_auth:resolution(). +respond_if_forbidden(forbidden, Response) -> + respond(Response); +respond_if_forbidden(allowed, _Response) -> + allowed. + %% get_auth_context(#{auth_context := AuthContext}) -> diff --git a/apps/capi/src/capi_handler_customers.erl b/apps/capi/src/capi_handler_customers.erl index 5e7f10c..edc1e5f 100644 --- a/apps/capi/src/capi_handler_customers.erl +++ b/apps/capi/src/capi_handler_customers.erl @@ -6,7 +6,11 @@ -export([prepare/3]). --import(capi_handler_utils, [general_error/2, logic_error/2]). +-import(capi_handler_utils, [ + general_error/2, + logic_error/2, + map_service_result/1 +]). -spec prepare( OperationID :: capi_handler:operation_id(), @@ -51,32 +55,29 @@ prepare('CreateCustomer' = OperationID, Req, Context) -> {ok, #{authorize => Authorize, process => Process}}; prepare('GetCustomerById' = OperationID, Req, Context) -> CustomerID = maps:get('customerID', Req), - CustomerReply = get_customer_by_id(CustomerID, Context), + Customer = map_service_result(get_customer_by_id(CustomerID, Context)), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, customer => CustomerID}}, - {payproc, #{customer => maybe_woody_reply(CustomerReply)}} + {payproc, #{customer => Customer}} ], - {ok, capi_auth:authorize_operation(Prototypes, Context)} + {ok, mask_customer_notfound(capi_auth:authorize_operation(Prototypes, Context))} end, Process = fun() -> - case CustomerReply of - {ok, Customer} -> + case Customer of + #payproc_Customer{} -> {ok, {200, #{}, decode_customer(Customer)}}; - {exception, #payproc_InvalidUser{}} -> - {ok, general_error(404, <<"Customer not found">>)}; - {exception, #payproc_CustomerNotFound{}} -> + undefined -> {ok, general_error(404, <<"Customer not found">>)} end end, {ok, #{authorize => Authorize, process => Process}}; prepare('DeleteCustomer' = OperationID, Req, Context) -> CustomerID = maps:get('customerID', Req), - CustomerReply = get_customer_by_id(CustomerID, Context), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, customer => CustomerID}}, - {payproc, #{customer => maybe_woody_reply(CustomerReply)}} + {payproc, #{customer => CustomerID}} ], {ok, capi_auth:authorize_operation(Prototypes, Context)} end, @@ -98,22 +99,20 @@ prepare('DeleteCustomer' = OperationID, Req, Context) -> {ok, #{authorize => Authorize, process => Process}}; prepare('CreateCustomerAccessToken' = OperationID, Req, Context) -> CustomerID = maps:get('customerID', Req), - CustomerReply = get_customer_by_id(CustomerID, Context), + Customer = map_service_result(get_customer_by_id(CustomerID, Context)), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, customer => CustomerID}}, - {payproc, #{customer => maybe_woody_reply(CustomerReply)}} + {payproc, #{customer => Customer}} ], {ok, capi_auth:authorize_operation(Prototypes, Context)} end, Process = fun() -> - case CustomerReply of - {ok, #payproc_Customer{owner_id = PartyID}} -> + case Customer of + #payproc_Customer{owner_id = PartyID} -> Response = capi_handler_utils:issue_access_token(PartyID, {customer, CustomerID}), {ok, {201, #{}, Response}}; - {exception, #payproc_InvalidUser{}} -> - {ok, general_error(404, <<"Customer not found">>)}; - {exception, #payproc_CustomerNotFound{}} -> + undefined -> {ok, general_error(404, <<"Customer not found">>)} end end, @@ -180,21 +179,19 @@ prepare('CreateBinding' = OperationID, Req, Context) -> {ok, #{authorize => Authorize, process => Process}}; prepare('GetBindings' = OperationID, Req, Context) -> CustomerID = maps:get(customerID, Req), - CustomerReply = get_customer_by_id(CustomerID, Context), + Customer = map_service_result(get_customer_by_id(CustomerID, Context)), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, customer => CustomerID}}, - {payproc, #{customer => maybe_woody_reply(CustomerReply)}} + {payproc, #{customer => Customer}} ], - {ok, capi_auth:authorize_operation(Prototypes, Context)} + {ok, mask_customer_notfound(capi_auth:authorize_operation(Prototypes, Context))} end, Process = fun() -> - case CustomerReply of - {ok, #payproc_Customer{bindings = Bindings}} -> + case Customer of + #payproc_Customer{bindings = Bindings} -> {ok, {200, #{}, [decode_customer_binding(B, Context) || B <- Bindings]}}; - {exception, #payproc_InvalidUser{}} -> - {ok, general_error(404, <<"Customer not found">>)}; - {exception, #payproc_CustomerNotFound{}} -> + undefined -> {ok, general_error(404, <<"Customer not found">>)} end end, @@ -202,26 +199,24 @@ prepare('GetBindings' = OperationID, Req, Context) -> prepare('GetBinding' = OperationID, Req, Context) -> CustomerID = maps:get(customerID, Req), CustomerBindingID = maps:get(customerBindingID, Req), - CustomerReply = get_customer_by_id(CustomerID, Context), + Customer = map_service_result(get_customer_by_id(CustomerID, Context)), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, customer => CustomerID, binding => CustomerBindingID}}, - {payproc, #{customer => maybe_woody_reply(CustomerReply)}} + {payproc, #{customer => Customer}} ], - {ok, capi_auth:authorize_operation(Prototypes, Context)} + {ok, mask_customer_notfound(capi_auth:authorize_operation(Prototypes, Context))} end, Process = fun() -> - case CustomerReply of - {ok, #payproc_Customer{bindings = Bindings}} -> + case Customer of + #payproc_Customer{bindings = Bindings} -> case lists:keyfind(CustomerBindingID, #payproc_CustomerBinding.id, Bindings) of #payproc_CustomerBinding{} = B -> {ok, {200, #{}, decode_customer_binding(B, Context)}}; false -> {ok, general_error(404, <<"Customer binding not found">>)} end; - {exception, #payproc_InvalidUser{}} -> - {ok, general_error(404, <<"Customer not found">>)}; - {exception, #payproc_CustomerNotFound{}} -> + undefined -> {ok, general_error(404, <<"Customer not found">>)} end end, @@ -233,7 +228,7 @@ prepare('GetCustomerEvents' = OperationID, Req, Context) -> {operation, #{id => OperationID, customer => CustomerID}}, {payproc, #{customer => CustomerID}} ], - {ok, capi_auth:authorize_operation(Prototypes, Context)} + {ok, mask_customer_notfound(capi_auth:authorize_operation(Prototypes, Context))} end, Process = fun() -> GetterFun = fun(Range) -> @@ -266,15 +261,19 @@ prepare(_OperationID, _Req, _Context) -> %% -maybe_woody_reply({ok, Reply}) -> - Reply; -maybe_woody_reply({exception, _}) -> - undefined. - get_customer_by_id(CustomerID, Context) -> EventRange = #payproc_EventRange{}, capi_handler_utils:service_call({customer_management, 'Get', {CustomerID, EventRange}}, Context). +mask_customer_notfound(Resolution) -> + % ED-206 + % When bouncer says "forbidden" we can't really tell the difference between "forbidden because + % of no such customer", "forbidden because client has no access to it" and "forbidden because + % client has no permission to act on it". From the point of view of existing integrations this + % is not great, so we have to mask specific instances of missing authorization as if specified + % customer is nonexistent. + capi_handler:respond_if_forbidden(Resolution, general_error(404, <<"Customer not found">>)). + generate_customer_id(OperationID, PartyID, CustomerParams, #{woody_context := WoodyContext}) -> ExternalID = maps:get(<<"externalID">>, CustomerParams, undefined), IdempKey = {OperationID, PartyID, ExternalID}, diff --git a/apps/capi/src/capi_handler_decoder_invoicing.erl b/apps/capi/src/capi_handler_decoder_invoicing.erl index 15b94d9..9ed33ce 100644 --- a/apps/capi/src/capi_handler_decoder_invoicing.erl +++ b/apps/capi/src/capi_handler_decoder_invoicing.erl @@ -139,7 +139,6 @@ decode_payment(InvoiceID, Payment, Context) -> <<"externalID">> => Payment#domain_InvoicePayment.external_id, <<"invoiceID">> => InvoiceID, <<"createdAt">> => Payment#domain_InvoicePayment.created_at, - % TODO whoops, nothing to get it from yet <<"flow">> => decode_flow(Payment#domain_InvoicePayment.flow), <<"amount">> => Amount, <<"currency">> => capi_handler_decoder_utils:decode_currency(Currency), diff --git a/apps/capi/src/capi_handler_invoice_templates.erl b/apps/capi/src/capi_handler_invoice_templates.erl index 4f8c520..f0b661a 100644 --- a/apps/capi/src/capi_handler_invoice_templates.erl +++ b/apps/capi/src/capi_handler_invoice_templates.erl @@ -7,7 +7,11 @@ -export([prepare/3]). --import(capi_handler_utils, [general_error/2, logic_error/2]). +-import(capi_handler_utils, [ + general_error/2, + logic_error/2, + map_service_result/1 +]). -spec prepare( OperationID :: capi_handler:operation_id(), @@ -63,23 +67,13 @@ prepare('CreateInvoiceTemplate' = OperationID, Req, Context) -> {ok, #{authorize => Authorize, process => Process}}; prepare('GetInvoiceTemplateByID' = OperationID, Req, Context) -> InvoiceTemplateID = maps:get('invoiceTemplateID', Req), - InvoiceTpl = - case get_invoice_template(InvoiceTemplateID, Context) of - {ok, Result} -> - Result; - {exception, E} when - E == #payproc_InvalidUser{}; - E == #payproc_InvoiceTemplateNotFound{}; - E == #payproc_InvoiceTemplateRemoved{} - -> - undefined - end, + InvoiceTpl = map_service_result(get_invoice_template(InvoiceTemplateID, Context)), Authorize = fun() -> Prototypes = [ {operation, #{invoice_template => InvoiceTemplateID, id => OperationID}}, {payproc, #{invoice_template => InvoiceTpl}} ], - Resolution = capi_auth:authorize_operation(Prototypes, Context), + Resolution = mask_invoice_template_notfound(capi_auth:authorize_operation(Prototypes, Context)), {ok, Resolution} end, Process = fun() -> @@ -89,22 +83,10 @@ prepare('GetInvoiceTemplateByID' = OperationID, Req, Context) -> {ok, #{authorize => Authorize, process => Process}}; prepare('UpdateInvoiceTemplate' = OperationID, Req, Context) -> InvoiceTemplateID = maps:get('invoiceTemplateID', Req), - InvoiceTpl = - case get_invoice_template(InvoiceTemplateID, Context) of - {ok, Result} -> - Result; - {exception, E} when - E == #payproc_InvalidUser{}; - E == #payproc_InvoiceTemplateNotFound{}; - E == #payproc_InvoiceTemplateRemoved{} - -> - undefined - end, Authorize = fun() -> - InvoiceTemplateID = maps:get('invoiceTemplateID', Req), Prototypes = [ {operation, #{invoice_template => InvoiceTemplateID, id => OperationID}}, - {payproc, #{invoice_template => InvoiceTpl}} + {payproc, #{invoice_template => InvoiceTemplateID}} ], Resolution = capi_auth:authorize_operation(Prototypes, Context), {ok, Resolution} @@ -180,13 +162,7 @@ prepare('DeleteInvoiceTemplate' = OperationID, Req, Context) -> {ok, #{authorize => Authorize, process => Process}}; prepare('CreateInvoiceWithTemplate' = OperationID, Req, Context) -> InvoiceTplID = maps:get('invoiceTemplateID', Req), - InvoiceTpl = - case get_invoice_template(InvoiceTplID, Context) of - {ok, Result} -> - Result; - {exception, _Exception} -> - undefined - end, + InvoiceTpl = map_service_result(get_invoice_template(InvoiceTplID, Context)), Authorize = fun() -> Prototypes = [ {operation, #{invoice_template => InvoiceTplID, id => OperationID}}, @@ -239,19 +215,13 @@ prepare('CreateInvoiceWithTemplate' = OperationID, Req, Context) -> {ok, #{authorize => Authorize, process => Process}}; prepare('GetInvoicePaymentMethodsByTemplateID' = OperationID, Req, Context) -> InvoiceTemplateID = maps:get('invoiceTemplateID', Req), - InvoiceTemplate = - case get_invoice_template(InvoiceTemplateID, Context) of - {ok, Result} -> - Result; - {exception, _Exception} -> - undefined - end, + InvoiceTemplate = map_service_result(get_invoice_template(InvoiceTemplateID, Context)), Authorize = fun() -> Prototypes = [ {operation, #{invoice_template => InvoiceTemplateID, id => OperationID}}, {payproc, #{invoice_template => InvoiceTemplate}} ], - Resolution = capi_auth:authorize_operation(Prototypes, Context), + Resolution = mask_invoice_template_notfound(capi_auth:authorize_operation(Prototypes, Context)), {ok, Resolution} end, Process = fun() -> @@ -277,6 +247,15 @@ prepare('GetInvoicePaymentMethodsByTemplateID' = OperationID, Req, Context) -> prepare(_OperationID, _Req, _Context) -> {error, noimpl}. +mask_invoice_template_notfound(Resolution) -> + % ED-206 + % When bouncer says "forbidden" we can't really tell the difference between "forbidden because + % of no such invoice teplate", "forbidden because client has no access to it" and "forbidden + % because client has no permission to act on it". From the point of view of existing integrations + % this is not great, so we have to mask specific instances of missing authorization as if + % specified invoice template is nonexistent. + capi_handler:respond_if_forbidden(Resolution, general_error(404, <<"Invoice template not found">>)). + %% create_invoice(PartyID, InvoiceTplID, InvoiceParams, Context, BenderPrefix) -> diff --git a/apps/capi/src/capi_handler_invoices.erl b/apps/capi/src/capi_handler_invoices.erl index 7b7a3fe..327ef06 100644 --- a/apps/capi/src/capi_handler_invoices.erl +++ b/apps/capi/src/capi_handler_invoices.erl @@ -6,7 +6,11 @@ -export([prepare/3]). --import(capi_handler_utils, [general_error/2, logic_error/2]). +-import(capi_handler_utils, [ + general_error/2, + logic_error/2, + map_service_result/1 +]). -spec prepare( OperationID :: capi_handler:operation_id(), @@ -68,13 +72,7 @@ prepare('CreateInvoice' = OperationID, Req, Context) -> {ok, #{authorize => Authorize, process => Process}}; prepare('CreateInvoiceAccessToken' = OperationID, Req, Context) -> InvoiceID = maps:get(invoiceID, Req), - ResultInvoice = - case capi_handler_utils:get_invoice_by_id(InvoiceID, Context) of - {ok, Result} -> - Result; - {exception, _Exception} -> - undefined - end, + ResultInvoice = map_service_result(capi_handler_utils:get_invoice_by_id(InvoiceID, Context)), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, invoice => InvoiceID}}, @@ -85,9 +83,8 @@ prepare('CreateInvoiceAccessToken' = OperationID, Req, Context) -> end, Process = fun() -> capi_handler:respond_if_undefined(ResultInvoice, general_error(404, <<"Invoice not found">>)), - #'payproc_Invoice'{invoice = Invoice} = ResultInvoice, + #payproc_Invoice{invoice = #domain_Invoice{owner_id = PartyID}} = ResultInvoice, ExtraProperties = capi_handler_utils:get_extra_properties(Context), - #'domain_Invoice'{owner_id = PartyID} = Invoice, Response = capi_handler_utils:issue_access_token( PartyID, {invoice, InvoiceID}, @@ -98,19 +95,13 @@ prepare('CreateInvoiceAccessToken' = OperationID, Req, Context) -> {ok, #{authorize => Authorize, process => Process}}; prepare('GetInvoiceByID' = OperationID, Req, Context) -> InvoiceID = maps:get(invoiceID, Req), - ResultInvoice = - case capi_handler_utils:get_invoice_by_id(InvoiceID, Context) of - {ok, Result} -> - Result; - {exception, _Exception} -> - undefined - end, + ResultInvoice = map_service_result(capi_handler_utils:get_invoice_by_id(InvoiceID, Context)), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, invoice => InvoiceID}}, {payproc, #{invoice => ResultInvoice}} ], - Resolution = capi_auth:authorize_operation(Prototypes, Context), + Resolution = mask_invoice_notfound(capi_auth:authorize_operation(Prototypes, Context)), {ok, Resolution} end, Process = fun() -> @@ -135,7 +126,7 @@ prepare('GetInvoiceByExternalID' = OperationID, Req, Context) -> {operation, #{id => OperationID, invoice => InvoiceID}}, {payproc, #{invoice => ResultInvoice}} ], - Resolution = capi_auth:authorize_operation(Prototypes, Context), + Resolution = mask_invoice_notfound(capi_auth:authorize_operation(Prototypes, Context)), {ok, Resolution} end, Process = fun() -> @@ -218,7 +209,7 @@ prepare('GetInvoiceEvents' = OperationID, Req, Context) -> {operation, #{id => OperationID, invoice => InvoiceID}}, {payproc, #{invoice => InvoiceID}} ], - Resolution = capi_auth:authorize_operation(Prototypes, Context), + Resolution = mask_invoice_notfound(capi_auth:authorize_operation(Prototypes, Context)), {ok, Resolution} end, Process = fun() -> @@ -256,19 +247,13 @@ prepare('GetInvoiceEvents' = OperationID, Req, Context) -> {ok, #{authorize => Authorize, process => Process}}; prepare('GetInvoicePaymentMethods' = OperationID, Req, Context) -> InvoiceID = maps:get(invoiceID, Req), - ResultInvoice = - case capi_handler_utils:get_invoice_by_id(InvoiceID, Context) of - {ok, Result} -> - Result; - {exception, _Exception} -> - undefined - end, + ResultInvoice = map_service_result(capi_handler_utils:get_invoice_by_id(InvoiceID, Context)), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, invoice => InvoiceID}}, {payproc, #{invoice => ResultInvoice}} ], - Resolution = capi_auth:authorize_operation(Prototypes, Context), + Resolution = mask_invoice_notfound(capi_auth:authorize_operation(Prototypes, Context)), {ok, Resolution} end, Process = fun() -> @@ -490,3 +475,12 @@ get_invoice_by_external_id(ExternalID, #{woody_context := WoodyContext} = Contex Error -> Error end. + +mask_invoice_notfound(Resolution) -> + % ED-206 + % When bouncer says "forbidden" we can't really tell the difference between "forbidden because + % of no such invoice", "forbidden because client has no access to it" and "forbidden because + % client has no permission to act on it". From the point of view of existing integrations this + % is not great, so we have to mask specific instances of missing authorization as if specified + % invoice is nonexistent. + capi_handler:respond_if_forbidden(Resolution, general_error(404, <<"Invoice not found">>)). diff --git a/apps/capi/src/capi_handler_parties.erl b/apps/capi/src/capi_handler_parties.erl index fe18816..7c6dc77 100644 --- a/apps/capi/src/capi_handler_parties.erl +++ b/apps/capi/src/capi_handler_parties.erl @@ -75,7 +75,7 @@ prepare('GetPartyByID' = OperationID, Req, Context) -> PartyID = maps:get(partyID, Req), Authorize = fun() -> Prototypes = [{operation, #{id => OperationID, party => PartyID}}], - {ok, capi_auth:authorize_operation(Prototypes, Context)} + {ok, mask_party_notfound(capi_auth:authorize_operation(Prototypes, Context))} end, Process = fun() -> case capi_party:get_party(PartyID, Context) of @@ -163,3 +163,12 @@ create_party(PartyID, Context) -> Error -> Error end. + +mask_party_notfound(Resolution) -> + % ED-206 + % When bouncer says "forbidden" we can't really tell the difference between "forbidden because + % of no such party", "forbidden because client has no access to it" and "forbidden because + % client has no permission to act on it". From the point of view of existing integrations this + % is not great, so we have to mask specific instances of missing authorization as if specified + % party is nonexistent. + capi_handler:respond_if_forbidden(Resolution, general_error(404, <<"Party not found">>)). diff --git a/apps/capi/src/capi_handler_payments.erl b/apps/capi/src/capi_handler_payments.erl index a97481e..32bcb62 100644 --- a/apps/capi/src/capi_handler_payments.erl +++ b/apps/capi/src/capi_handler_payments.erl @@ -17,7 +17,7 @@ ) -> {ok, capi_handler:request_state()} | {error, noimpl}. prepare(OperationID = 'CreatePayment', Req, Context) -> InvoiceID = maps:get(invoiceID, Req), - Invoice = map_result(get_invoice_by_id(InvoiceID, Context)), + Invoice = get_invoice_by_id(InvoiceID, Context), PaymentParams = maps:get('PaymentParams', Req), Authorize = fun() -> Prototypes = [ @@ -90,13 +90,13 @@ prepare(OperationID = 'CreatePayment', Req, Context) -> {ok, #{authorize => Authorize, process => Process}}; prepare(OperationID = 'GetPayments', Req, Context) -> InvoiceID = maps:get(invoiceID, Req), - Invoice = map_result(get_invoice_by_id(InvoiceID, Context)), + Invoice = get_invoice_by_id(InvoiceID, Context), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, invoice => InvoiceID}}, {payproc, #{invoice => Invoice}} ], - {ok, capi_auth:authorize_operation(Prototypes, Context)} + {ok, mask_invoice_notfound(capi_auth:authorize_operation(Prototypes, Context))} end, Process = fun() -> capi_handler:respond_if_undefined(Invoice, general_error(404, <<"Invoice not found">>)), @@ -107,31 +107,30 @@ prepare(OperationID = 'GetPayments', Req, Context) -> prepare(OperationID = 'GetPaymentByID', Req, Context) -> InvoiceID = maps:get(invoiceID, Req), PaymentID = maps:get(paymentID, Req), - Invoice = map_result(get_invoice_by_id(InvoiceID, Context)), + Invoice = get_invoice_by_id(InvoiceID, Context), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, invoice => InvoiceID, payment => PaymentID}}, {payproc, #{invoice => Invoice}} ], - {ok, capi_auth:authorize_operation(Prototypes, Context)} + {ok, mask_invoice_notfound(capi_auth:authorize_operation(Prototypes, Context))} end, Process = fun() -> capi_handler:respond_if_undefined(Invoice, general_error(404, <<"Invoice not found">>)), case find_payment_by_id(PaymentID, Invoice) of - {ok, Payment} -> + Payment when Payment /= undefined -> {ok, {200, #{}, decode_invoice_payment(InvoiceID, Payment, Context)}}; - {error, payment_not_found} -> + undefined -> {ok, general_error(404, <<"Payment not found">>)} end end, {ok, #{authorize => Authorize, process => Process}}; prepare(OperationID = 'GetPaymentByExternalID', Req, Context) -> ExternalID = maps:get(externalID, Req), - InternalID = map_result(get_payment_by_external_id(ExternalID, Context)), - Invoice = map_result( - maybe(InternalID, fun({InvoiceID, _}) -> - get_invoice_by_id(InvoiceID, Context) - end) + InternalID = get_payment_by_external_id(ExternalID, Context), + Invoice = maybe( + InternalID, + fun({InvoiceID, _}) -> get_invoice_by_id(InvoiceID, Context) end ), OperationPrototype = maybe( @@ -146,17 +145,16 @@ prepare(OperationID = 'GetPaymentByExternalID', Req, Context) -> {operation, genlib:define(OperationPrototype, #{id => OperationID})}, {payproc, #{invoice => Invoice}} ], - {ok, capi_auth:authorize_operation(Prototypes, Context)} + {ok, mask_payment_notfound(capi_auth:authorize_operation(Prototypes, Context))} end, Process = fun() -> capi_handler:respond_if_undefined(InternalID, general_error(404, <<"Payment not found">>)), capi_handler:respond_if_undefined(Invoice, general_error(404, <<"Invoice not found">>)), - {InvoiceID, PaymentID} = InternalID, case find_payment_by_id(PaymentID, Invoice) of - {ok, Payment} -> + Payment when Payment /= undefined -> {ok, {200, #{}, decode_invoice_payment(InvoiceID, Payment, Context)}}; - {error, payment_not_found} -> + undefined -> {ok, general_error(404, <<"Payment not found">>)} end end, @@ -164,7 +162,7 @@ prepare(OperationID = 'GetPaymentByExternalID', Req, Context) -> prepare(OperationID = 'CapturePayment', Req, Context) -> InvoiceID = maps:get(invoiceID, Req), PaymentID = maps:get(paymentID, Req), - Invoice = map_result(get_invoice_by_id(InvoiceID, Context)), + Invoice = get_invoice_by_id(InvoiceID, Context), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, invoice => InvoiceID, payment => PaymentID}}, @@ -231,11 +229,10 @@ prepare(OperationID = 'CapturePayment', Req, Context) -> prepare(OperationID = 'CancelPayment', Req, Context) -> InvoiceID = maps:get(invoiceID, Req), PaymentID = maps:get(paymentID, Req), - Invoice = map_result(get_invoice_by_id(InvoiceID, Context)), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, invoice => InvoiceID, payment => PaymentID}}, - {payproc, #{invoice => Invoice}} + {payproc, #{invoice => InvoiceID}} ], {ok, capi_auth:authorize_operation(Prototypes, Context)} end, @@ -351,24 +348,24 @@ prepare(OperationID = 'CreateRefund', Req, Context) -> prepare(OperationID = 'GetRefunds', Req, Context) -> InvoiceID = maps:get(invoiceID, Req), PaymentID = maps:get(paymentID, Req), - Invoice = map_result(get_invoice_by_id(InvoiceID, Context)), + Invoice = get_invoice_by_id(InvoiceID, Context), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, invoice => InvoiceID, payment => PaymentID}}, {payproc, #{invoice => Invoice}} ], - {ok, capi_auth:authorize_operation(Prototypes, Context)} + {ok, mask_invoice_notfound(capi_auth:authorize_operation(Prototypes, Context))} end, Process = fun() -> capi_handler:respond_if_undefined(Invoice, general_error(404, <<"Invoice not found">>)), case find_payment_by_id(PaymentID, Invoice) of - {ok, #payproc_InvoicePayment{refunds = Refunds}} -> + #payproc_InvoicePayment{refunds = Refunds} -> {ok, {200, #{}, [ capi_handler_decoder_invoicing:decode_refund(R, Context) || #payproc_InvoicePaymentRefund{refund = R} <- Refunds ]}}; - {error, payment_not_found} -> + undefined -> {ok, general_error(404, <<"Payment not found">>)} end end, @@ -377,14 +374,14 @@ prepare(OperationID = 'GetRefundByID', Req, Context) -> InvoiceID = maps:get(invoiceID, Req), PaymentID = maps:get(paymentID, Req), RefundID = maps:get(refundID, Req), - Invoice = map_result(get_invoice_by_id(InvoiceID, Context)), - Payment = map_result(find_payment_by_id(PaymentID, Invoice)), + Invoice = get_invoice_by_id(InvoiceID, Context), + Payment = find_payment_by_id(PaymentID, Invoice), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, invoice => InvoiceID, payment => PaymentID, refund => RefundID}}, {payproc, #{invoice => Invoice}} ], - {ok, capi_auth:authorize_operation(Prototypes, Context)} + {ok, mask_invoice_notfound(capi_auth:authorize_operation(Prototypes, Context))} end, Process = fun() -> @@ -392,44 +389,45 @@ prepare(OperationID = 'GetRefundByID', Req, Context) -> capi_handler:respond_if_undefined(Payment, general_error(404, <<"Payment not found">>)), case find_refund_by_id(RefundID, Payment) of - {ok, #payproc_InvoicePaymentRefund{refund = Refund}} -> + #payproc_InvoicePaymentRefund{refund = Refund} -> {ok, {200, #{}, capi_handler_decoder_invoicing:decode_refund(Refund, Context)}}; - {error, refund_not_found} -> + undefined -> {ok, general_error(404, <<"Invoice payment refund not found">>)} end end, {ok, #{authorize => Authorize, process => Process}}; prepare(OperationID = 'GetRefundByExternalID', Req, Context) -> ExternalID = maps:get(externalID, Req), - InternalID = map_result(get_refund_by_external_id(ExternalID, Context)), - Invoice = map_result( - maybe(InternalID, fun({InvoiceID, _PaymentID, _RefundID}) -> - get_invoice_by_id(InvoiceID, Context) - end) + InternalID = get_refund_by_external_id(ExternalID, Context), + Invoice = maybe( + InternalID, + fun({InvoiceID, _PaymentID, _RefundID}) -> get_invoice_by_id(InvoiceID, Context) end + ), + OperationPrototype = maybe( + InternalID, + fun({InvoiceID, PaymentID, RefundID}) -> + #{id => OperationID, invoice => InvoiceID, payment => PaymentID, refund => RefundID} + end ), - OperationPrototype = maybe(InternalID, fun({InvoiceID, PaymentID, RefundID}) -> - #{id => OperationID, invoice => InvoiceID, payment => PaymentID, refund => RefundID} - end), Authorize = fun() -> Prototypes = [ {operation, genlib:define(OperationPrototype, #{id => OperationID})}, {payproc, #{invoice => Invoice}} ], - {ok, capi_auth:authorize_operation(Prototypes, Context)} + {ok, mask_refund_notfound(capi_auth:authorize_operation(Prototypes, Context))} end, Process = fun() -> capi_handler:respond_if_undefined(InternalID, general_error(404, <<"Refund not found">>)), capi_handler:respond_if_undefined(Invoice, general_error(404, <<"Invoice not found">>)), {_InvoiceID, PaymentID, RefundID} = InternalID, - Payment = map_result(find_payment_by_id(PaymentID, Invoice)), + Payment = find_payment_by_id(PaymentID, Invoice), capi_handler:respond_if_undefined(Payment, general_error(404, <<"Payment not found">>)), - case find_refund_by_id(RefundID, Payment) of - {ok, #payproc_InvoicePaymentRefund{refund = Refund}} -> + #payproc_InvoicePaymentRefund{refund = Refund} -> {ok, {200, #{}, capi_handler_decoder_invoicing:decode_refund(Refund, Context)}}; - {error, refund_not_found} -> + undefined -> {ok, general_error(404, <<"Invoice payment refund not found">>)} end end, @@ -437,7 +435,7 @@ prepare(OperationID = 'GetRefundByExternalID', Req, Context) -> prepare(OperationID = 'GetChargebacks', Req, Context) -> InvoiceID = maps:get(invoiceID, Req), PaymentID = maps:get(paymentID, Req), - Invoice = map_result(get_invoice_by_id(InvoiceID, Context)), + Invoice = get_invoice_by_id(InvoiceID, Context), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, invoice => InvoiceID, payment => PaymentID}}, @@ -451,9 +449,9 @@ prepare(OperationID = 'GetChargebacks', Req, Context) -> capi_handler_decoder_invoicing:decode_chargeback(C#payproc_InvoicePaymentChargeback.chargeback, Context) end, case find_payment_by_id(PaymentID, Invoice) of - {ok, #payproc_InvoicePayment{chargebacks = Chargebacks}} -> + #payproc_InvoicePayment{chargebacks = Chargebacks} -> {ok, {200, #{}, [DecodeChargebackFun(C) || C <- Chargebacks]}}; - {error, payment_not_found} -> + undefined -> {ok, general_error(404, <<"Payment not found">>)} end end, @@ -462,9 +460,8 @@ prepare(OperationID = 'GetChargebackByID', Req, Context) -> InvoiceID = maps:get(invoiceID, Req), PaymentID = maps:get(paymentID, Req), ChargebackID = maps:get(chargebackID, Req), - Invoice = map_result(get_invoice_by_id(InvoiceID, Context)), - Payment = map_result(find_payment_by_id(PaymentID, Invoice)), - + Invoice = get_invoice_by_id(InvoiceID, Context), + Payment = find_payment_by_id(PaymentID, Invoice), Authorize = fun() -> Prototypes = [ {operation, #{id => OperationID, invoice => InvoiceID, payment => PaymentID}}, @@ -517,23 +514,13 @@ find_payment_by_id(PaymentID, #payproc_Invoice{payments = Payments}) -> Fun = fun(#payproc_InvoicePayment{payment = #domain_InvoicePayment{id = ID}}) -> PaymentID == ID end, - case find_by(Fun, genlib:define(Payments, [])) of - undefined -> - {error, payment_not_found}; - Payment -> - {ok, Payment} - end. + find_by(Fun, genlib:define(Payments, [])). find_refund_by_id(RefundID, #payproc_InvoicePayment{refunds = Refunds}) -> Fun = fun(#payproc_InvoicePaymentRefund{refund = Refund}) -> Refund#domain_InvoicePaymentRefund.id == RefundID end, - case find_by(Fun, genlib:define(Refunds, [])) of - undefined -> - {error, refund_not_found}; - Refund -> - {ok, Refund} - end. + find_by(Fun, genlib:define(Refunds, [])). find_chargeback_by_id(ChargebackID, #payproc_InvoicePayment{chargebacks = Chargebacks}) -> Fun = fun(#payproc_InvoicePaymentChargeback{chargeback = Chargeback}) -> @@ -557,18 +544,13 @@ find_by(_, []) -> get_invoice_by_id(InvoiceID, Context) -> case capi_handler_utils:get_invoice_by_id(InvoiceID, Context) of {ok, Invoice} -> - {ok, Invoice}; + Invoice; {exception, #payproc_InvalidUser{}} -> - {error, invalid_user}; + undefined; {exception, #payproc_InvoiceNotFound{}} -> - {error, invoice_not_found} + undefined end. -map_result({ok, Value}) -> - Value; -map_result(_) -> - undefined. - decrypt_payer(#{<<"payerType">> := <<"PaymentResourcePayer">>} = Payer) -> #{<<"paymentToolToken">> := Token} = Payer, Payer2 = maps:without([<<"paymentToolToken">>], Payer), @@ -695,23 +677,22 @@ get_refund_by_external_id(ExternalID, #{woody_context := WoodyContext} = Context {ok, RefundID, CtxData} -> InvoiceID = maps:get(<<"invoice_id">>, CtxData), PaymentID = maps:get(<<"payment_id">>, CtxData), - {ok, {InvoiceID, PaymentID, RefundID}}; - {error, internal_id_not_found} = Error -> - Error + {InvoiceID, PaymentID, RefundID}; + {error, internal_id_not_found} -> + undefined end. -spec get_payment_by_external_id(binary(), capi_handler:processing_context()) -> - {ok, {binary(), binary()}} - | {error, internal_id_not_found}. + {binary(), binary()} | undefined. get_payment_by_external_id(ExternalID, #{woody_context := WoodyContext} = Context) -> PartyID = capi_handler_utils:get_party_id(Context), IdempotentKey = {'CreatePayment', PartyID, ExternalID}, case capi_bender:get_internal_id(IdempotentKey, WoodyContext) of {ok, PaymentID, CtxData} -> InvoiceID = maps:get(<<"invoice_id">>, CtxData), - {ok, {InvoiceID, PaymentID}}; - {error, internal_id_not_found} = Error -> - Error + {InvoiceID, PaymentID}; + {error, internal_id_not_found} -> + undefined end. encode_processing_deadline(Deadline) -> @@ -758,6 +739,22 @@ refund_payment(RefundID, InvoiceID, PaymentID, RefundParams, Context) -> Call = {invoicing, 'RefundPayment', CallArgs}, capi_handler_utils:service_call_with([user_info], Call, Context). +%% ED-206 +%% When bouncer says "forbidden" we can't really tell the difference between "forbidden because +%% of no such invoice", "forbidden because client has no access to it" and "forbidden because +%% client has no permission to act on it". From the point of view of existing integrations this +%% is not great, so we have to mask specific instances of missing authorization as if specified +%% invoice / payment / refund is nonexistent. + +mask_invoice_notfound(Resolution) -> + capi_handler:respond_if_forbidden(Resolution, general_error(404, <<"Invoice not found">>)). + +mask_payment_notfound(Resolution) -> + capi_handler:respond_if_forbidden(Resolution, general_error(404, <<"Payment not found">>)). + +mask_refund_notfound(Resolution) -> + capi_handler:respond_if_forbidden(Resolution, general_error(404, <<"Invoice payment refund not found">>)). + %% create_sequence_id([Identifier | Rest], BenderPrefix) -> diff --git a/apps/capi/src/capi_handler_shops.erl b/apps/capi/src/capi_handler_shops.erl index 12dc4e6..81b46bd 100644 --- a/apps/capi/src/capi_handler_shops.erl +++ b/apps/capi/src/capi_handler_shops.erl @@ -89,10 +89,6 @@ prepare(OperationID = 'GetShopsForParty', Req, Context) -> {ok, capi_auth:authorize_operation(Prototypes, Context)} end, Process = fun() -> - % TODO - % Here we're relying on hellgate ownership check, thus no explicit authorization. - % Hovewer we're going to drop hellgate authz eventually, then we'll need to make sure that operation - % remains authorized. case capi_party:get_party(PartyID, Context) of {ok, Party} -> {ok, {200, #{}, decode_shops_map(Party#domain_Party.shops)}}; @@ -111,10 +107,6 @@ prepare(OperationID = 'GetShopByIDForParty', Req, Context) -> {ok, capi_auth:authorize_operation(Prototypes, Context)} end, Process = fun() -> - % TODO - % Here we're relying on hellgate ownership check, thus no explicit authorization. - % Hovewer we're going to drop hellgate authz eventually, then we'll need to make sure that operation - % remains authorized. case capi_party:get_shop(PartyID, ShopID, Context) of {ok, Shop} -> {ok, {200, #{}, decode_shop(Shop)}}; @@ -135,10 +127,6 @@ prepare(OperationID = 'ActivateShopForParty', Req, Context) -> {ok, capi_auth:authorize_operation(Prototypes, Context)} end, Process = fun() -> - % TODO - % Here we're relying on hellgate ownership check, thus no explicit authorization. - % Hovewer we're going to drop hellgate authz eventually, then we'll need to make sure that operation - % remains authorized. case capi_party:activate_shop(PartyID, ShopID, Context) of ok -> {ok, {204, #{}, undefined}}; diff --git a/apps/capi/src/capi_handler_utils.erl b/apps/capi/src/capi_handler_utils.erl index 6763329..9f03bde 100644 --- a/apps/capi/src/capi_handler_utils.erl +++ b/apps/capi/src/capi_handler_utils.erl @@ -12,6 +12,7 @@ -export([service_call_with/3]). -export([service_call/2]). +-export([map_service_result/1]). -export([get_auth_context/1]). -export([get_user_info/1]). @@ -99,6 +100,13 @@ service_call_with_([], Call, Context) -> service_call({ServiceName, Function, Args}, #{woody_context := WoodyContext}) -> capi_woody_client:call_service(ServiceName, Function, Args, WoodyContext). +-spec map_service_result({ok, Result} | {exception, woody_error:business_error()}) -> + Result | undefined. +map_service_result({ok, Result}) -> + Result; +map_service_result({exception, _}) -> + undefined. + -spec get_auth_context(processing_context()) -> any(). get_auth_context(#{swagger_context := #{auth_context := AuthContext}}) -> AuthContext. diff --git a/apps/capi/src/capi_sup.erl b/apps/capi/src/capi_sup.erl index 62324d7..04e5649 100644 --- a/apps/capi/src/capi_sup.erl +++ b/apps/capi/src/capi_sup.erl @@ -33,7 +33,7 @@ init([]) -> {LogicHandler, []} = get_logic_handler_info(#{party_client => PartyClient}), AdditionalRoutes = [{'_', [erl_health_handle:get_route(HealthCheck), get_prometheus_route()]}], SwaggerHandlerOpts = genlib_app:env(?APP, swagger_handler_opts, #{}), - SwaggerSpec = capi_swagger_server:child_spec({AdditionalRoutes, LogicHandler, SwaggerHandlerOpts}), + SwaggerSpec = capi_swagger_server:child_spec(AdditionalRoutes, LogicHandler, SwaggerHandlerOpts), {ok, { {one_for_all, 0, 1}, @@ -43,12 +43,7 @@ init([]) -> -spec get_logic_handler_info(capi_handler:handler_opts()) -> {Handler :: swag_server:logic_handler(_), [Spec :: supervisor:child_spec()] | []}. get_logic_handler_info(HandlerOpts) -> - case genlib_app:env(?APP, service_type) of - real -> - {{capi_handler, HandlerOpts}, []}; - undefined -> - exit(undefined_service_type) - end. + {{capi_handler, HandlerOpts}, []}. -spec enable_health_logging(erl_health:check()) -> erl_health:check(). enable_health_logging(Check) -> diff --git a/apps/capi/src/capi_swagger_server.erl b/apps/capi/src/capi_swagger_server.erl index 02e04ee..425343c 100644 --- a/apps/capi/src/capi_swagger_server.erl +++ b/apps/capi/src/capi_swagger_server.erl @@ -1,6 +1,6 @@ -module(capi_swagger_server). --export([child_spec/1]). +-export([child_spec/3]). -define(APP, capi). -define(DEFAULT_ACCEPTORS_POOLSIZE, 100). @@ -8,16 +8,9 @@ -define(DEFAULT_PORT, 8080). -define(RANCH_REF, ?MODULE). --define(START_TIME_TAG, processing_start_time). - --type params() :: { - cowboy_router:routes(), - swag_server:logic_handler(_), - swag_server_router:swagger_handler_opts() -}. - --spec child_spec(params()) -> supervisor:child_spec(). -child_spec({AdditionalRoutes, LogicHandler, SwaggerHandlerOpts}) -> +-spec child_spec(cowboy_router:routes(), swag_server:logic_handler(_), swag_server_router:swagger_handler_opts()) -> + supervisor:child_spec(). +child_spec(AdditionalRoutes, LogicHandler, SwaggerHandlerOpts) -> {Transport, TransportOpts} = get_socket_transport(), CowboyOpts = get_cowboy_config(AdditionalRoutes, LogicHandler, SwaggerHandlerOpts), GsTimeout = genlib_app:env(?APP, graceful_shutdown_timeout, 5000), diff --git a/apps/capi/test/capi_authorization_tests_SUITE.erl b/apps/capi/test/capi_authorization_tests_SUITE.erl index 9402d11..abf2117 100644 --- a/apps/capi/test/capi_authorization_tests_SUITE.erl +++ b/apps/capi/test/capi_authorization_tests_SUITE.erl @@ -3,6 +3,7 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("capi_dummy_data.hrl"). +-include_lib("damsel/include/dmsl_payment_processing_thrift.hrl"). -include_lib("jose/include/jose_jwk.hrl"). -export([all/0]). @@ -22,6 +23,15 @@ authorization_bad_token_error_test/1 ]). +-export([ + get_party_forbidden_notfound/1, + get_invoice_forbidden_notfound/1, + get_invoice_by_external_id_forbidden_notfound/1, + get_payment_by_external_id_forbidden_notfound/1, + get_refund_by_external_id_forbidden_notfound/1, + get_customer_forbidden_notfound/1 +]). + -define(emptyresp(Code), {error, {Code, #{}}}). -type test_case_name() :: atom(). @@ -37,7 +47,8 @@ init([]) -> -spec all() -> [{group, test_case_name()}]. all() -> [ - {group, authorization} + {group, authorization}, + {group, forbidden_masking} ]. -spec groups() -> [{group_name(), list(), [test_case_name()]}]. @@ -47,6 +58,14 @@ groups() -> authorization_error_no_header_test, authorization_error_no_permission_test, authorization_bad_token_error_test + ]}, + {forbidden_masking, [], [ + get_party_forbidden_notfound, + get_invoice_forbidden_notfound, + get_invoice_by_external_id_forbidden_notfound, + get_payment_by_external_id_forbidden_notfound, + get_refund_by_external_id_forbidden_notfound, + get_customer_forbidden_notfound ]} ]. @@ -63,42 +82,45 @@ end_per_suite(C) -> _ = [application:stop(App) || App <- proplists:get_value(apps, C)], ok. --spec init_per_group(group_name(), config()) -> config(). -init_per_group(_, Config) -> - SupPid = capi_ct_helper:start_mocked_service_sup(?MODULE), - Apps1 = capi_ct_helper_tk:mock_service(capi_ct_helper_tk:user_session_handler(), SupPid), - [{group_apps, Apps1}, {group_test_sup, SupPid} | Config]. - --spec end_per_group(group_name(), config()) -> _. -end_per_group(_Group, C) -> - _ = capi_utils:maybe(?config(group_test_sup, C), fun capi_ct_helper:stop_mocked_service_sup/1), - ok. - -spec init_per_testcase(test_case_name(), config()) -> config(). -init_per_testcase(authorization_error_no_permission_test, C) -> - SupPid = capi_ct_helper:start_mocked_service_sup(?MODULE), - Apps0 = capi_ct_helper_bouncer:mock_arbiter(capi_ct_helper_bouncer:judge_always_forbidden(), SupPid), - [{test_apps, Apps0}, {test_sup, SupPid} | C]; init_per_testcase(_Name, C) -> - SupPid = capi_ct_helper:start_mocked_service_sup(?MODULE), - Apps0 = capi_ct_helper_bouncer:mock_arbiter(capi_ct_helper_bouncer:judge_always_allowed(), SupPid), - [{test_apps, Apps0}, {test_sup, SupPid} | C]. + [{test_sup, capi_ct_helper:start_mocked_service_sup(?MODULE)} | C]. -spec end_per_testcase(test_case_name(), config()) -> _. end_per_testcase(_Name, C) -> capi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)), ok. +-spec init_per_group(group_name(), config()) -> config(). +init_per_group(GroupName, Config) -> + SupPid = capi_ct_helper:start_mocked_service_sup(?MODULE), + Apps1 = capi_ct_helper_tk:mock_service(capi_ct_helper_tk:user_session_handler(), SupPid), + Apps2 = + case GroupName of + forbidden_masking -> + capi_ct_helper_bouncer:mock_arbiter(capi_ct_helper_bouncer:judge_always_forbidden(), SupPid); + _ -> + [] + end, + [{group_apps, Apps1 ++ Apps2}, {group_test_sup, SupPid} | Config]. + +-spec end_per_group(group_name(), config()) -> _. +end_per_group(_Group, C) -> + _ = capi_utils:maybe(?config(group_test_sup, C), fun capi_ct_helper:stop_mocked_service_sup/1), + ok. + %%% Tests -spec authorization_error_no_header_test(config()) -> _. -authorization_error_no_header_test(_Config) -> +authorization_error_no_header_test(Config) -> Token = <<>>, + _ = capi_ct_helper_bouncer:mock_arbiter(capi_ct_helper_bouncer:judge_always_allowed(), Config), ?emptyresp(401) = capi_client_categories:get_categories(capi_ct_helper:get_context(Token)). -spec authorization_error_no_permission_test(config()) -> _. -authorization_error_no_permission_test(_Config) -> +authorization_error_no_permission_test(Config) -> Token = capi_ct_helper:issue_token(unlimited), + _ = capi_ct_helper_bouncer:mock_arbiter(capi_ct_helper_bouncer:judge_always_forbidden(), Config), ?emptyresp(401) = capi_client_parties:get_my_party(capi_ct_helper:get_context(Token)). -spec authorization_bad_token_error_test(config()) -> _. @@ -128,3 +150,68 @@ issue_dummy_token(ACL, Config) -> JWT = jose_jwt:sign(BadJWK, #{<<"alg">> => <<"RS256">>, <<"kid">> => KID}, Claims), {_Modules, Token} = jose_jws:compact(JWT), {ok, Token}. + +%%% + +-spec get_party_forbidden_notfound(config()) -> _. +-spec get_invoice_forbidden_notfound(config()) -> _. +-spec get_invoice_by_external_id_forbidden_notfound(config()) -> _. +-spec get_payment_by_external_id_forbidden_notfound(config()) -> _. +-spec get_refund_by_external_id_forbidden_notfound(config()) -> _. +-spec get_customer_forbidden_notfound(config()) -> _. + +get_party_forbidden_notfound(Config) -> + PartyID = <<"NONEXISTENT">>, + _ = capi_ct_helper:mock_services( + [ + {party_management, fun + ('GetRevision', _) -> {ok, ?INTEGER}; + ('Checkout', _) -> {throwing, #payproc_PartyNotFound{}} + end} + ], + Config + ), + {error, {404, _}} = capi_client_parties:get_party_by_id(mk_context(), PartyID). + +get_invoice_forbidden_notfound(Config) -> + InvoiceID = <<"NONEXISTENT">>, + _ = capi_ct_helper:mock_services( + [{invoicing, fun('Get', _) -> {throwing, #payproc_InvoiceNotFound{}} end}], + Config + ), + {error, {404, _}} = capi_client_invoices:get_invoice_by_id(mk_context(), InvoiceID). + +get_invoice_by_external_id_forbidden_notfound(Config) -> + ExternalID = <<"NEVERWAS">>, + _ = capi_ct_helper:mock_services( + [{bender, fun('GetInternalID', _) -> {throwing, capi_ct_helper_bender:no_internal_id()} end}], + Config + ), + {error, {404, _}} = capi_client_invoices:get_invoice_by_external_id(mk_context(), ExternalID). + +get_payment_by_external_id_forbidden_notfound(Config) -> + ExternalID = <<"NEVERWAS">>, + _ = capi_ct_helper:mock_services( + [{bender, fun('GetInternalID', _) -> {throwing, capi_ct_helper_bender:no_internal_id()} end}], + Config + ), + {error, {404, _}} = capi_client_payments:get_payment_by_external_id(mk_context(), ExternalID). + +get_refund_by_external_id_forbidden_notfound(Config) -> + ExternalID = <<"WHEREDIDYOUGETIT">>, + _ = capi_ct_helper:mock_services( + [{bender, fun('GetInternalID', _) -> {throwing, capi_ct_helper_bender:no_internal_id()} end}], + Config + ), + {error, {404, _}} = capi_client_payments:get_refund_by_external_id(mk_context(), ExternalID). + +get_customer_forbidden_notfound(Config) -> + CustomerID = <<"NONEXISTENT">>, + _ = capi_ct_helper:mock_services( + [{customer_management, fun('Get', _) -> {throwing, #payproc_CustomerNotFound{}} end}], + Config + ), + {error, {404, _}} = capi_client_customers:get_customer_by_id(mk_context(), CustomerID). + +mk_context() -> + capi_ct_helper:get_context(capi_ct_helper:issue_token(unlimited)). diff --git a/apps/capi/test/capi_ct_helper.erl b/apps/capi/test/capi_ct_helper.erl index b3a9567..c9c7bad 100644 --- a/apps/capi/test/capi_ct_helper.erl +++ b/apps/capi/test/capi_ct_helper.erl @@ -45,7 +45,10 @@ init_suite(Module, Config, CapiEnv) -> { 'Repository', {dmsl_domain_config_thrift, 'Repository'}, - fun('Checkout', _) -> {ok, ?SNAPSHOT} end + fun + ('Checkout', _) -> {ok, ?SNAPSHOT}; + ('PullRange', _) -> {ok, #{}} + end } ], SupPid