From 62b54961f4e0311767d55bc95432f3c06786971f Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Fri, 27 Oct 2023 01:52:04 +0200 Subject: [PATCH] Implement Redsys request signature in PostgreSQL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every company need to have its own merchant code and encryption key, thus it is not possible to use environment variables to keep that data, and i have to store it in the database. I do not want to give SELECT permission on the encryption key to guest, because i am going to fuck it up sooner or later, and everyone would be able to read that secret; i know it would. Therefore, i need a security definer function that takes the data to encrypt, use the key to encrypt it, and returns the result; nobody else should have access to that key, not even admins! By the way, i found out that every merchant receives the same key, thus it is not a problem to keep it in the repository. Since i need that SQL function to encrypt the data, i thought that i may go the whole nine yards and sign the request in PostgreSQL too, after all the data to sign comes from there, and it has JSON functions to create and base64-code an object. Fortunately, pg_crypto has all the functions that i need, but i can no longer keep that extension inside the auth schema, because it is used from others, and the public schema, like every other extensions, seems more appropriate. Instead of having the list of currency and language codes that Redsys uses as constants in the code, i moved that as field to the currency and language relations, so i can simply pass the lang_tag to the function and it can transform that tag to the correct code; the currency is from the company’s relation, since it is the only currency used in the whole application (for now). As a consequence, i had to grant execute to currency and the parse_price functions to guest, too. To generate the test data used in the unit tests, i used a third-party PHP implementation[0], but i only got from that the resulting base64-coded JSON object and signature, using the same that as in the unit test, and did not use any code from there. PostgreSQL formats the JSON as text differently than most implementations i have seen: it adds spaces between the key name and the colons, and space between the value and the separating comma. The first implementation used replace() to format the JSON as exactly as the PHP implementation, so that the result matches, and then tried to do generate the form by hand using the output from PostgreSQL without the replace(), to verify that Redsys would still accept my input. Finally, i adjusted the unit test to whatever pg_prove said it was getting from the function. I still have the form’s action hard-codded to the test environment, but the idea is that administrators should be able to switch from test to live themselves. That means that i need that info in the redsys relation as well. I think this is one of the few use cases for SQL’s types, because it is unlikely to change anytime soon, and i do not need the actual labels. Unfortunately, i could not use enumerations for the request’s transaction type because i can not attach an arbitrary number to the enum’s values. Having a relation is overkill, because i would need a constant in Go to refer to its primary key anyway, thus i kept the same constant i had before for that. Language and currency constant went out, as this is in the corresponding relations. In setup_redsys i must have a separate update if the encrypt_key is null because PostgreSQL checks constraints before key conflict, and i do not want to give a default value to the key if the row is not there yet. The problem is that i want to use null to mean “keep the same password”, because it is what i intend to do with the user-facing form: leave the input empty to keep the same password. As now Go needs to pass composite types back and forth with PostgreSQL, i need to register these types, and i have to do it every time because the only moment i have access to the non-pooled connection is in the AfterConnect function, but at that point i have no idea whether the user is going to request a payment. I do not know how much the performance degrades because of this. [0]: https://github.com/ssheduardo/sermepa/blob/master/src/Sermepa/Tpv/Tpv.php --- demo/demo.sql | 2 + deploy/available_currencies.sql | 6 +- deploy/available_languages.sql | 10 +- deploy/currency.sql | 4 +- deploy/encode_base64url.sql | 22 +++ deploy/encrypt_password.sql | 2 +- deploy/extension_pgcrypto.sql | 2 +- deploy/language.sql | 3 +- deploy/login.sql | 4 +- deploy/parse_price.sql | 1 + deploy/redsys.sql | 61 ++++++++ deploy/redsys_encrypt.sql | 29 ++++ deploy/redsys_environment.sql | 10 ++ deploy/redsys_integration.sql | 13 ++ deploy/redsys_request.sql | 21 +++ deploy/redsys_sign_request.sql | 68 +++++++++ deploy/redsys_signed_request.sql | 14 ++ deploy/setup_redsys.sql | 43 ++++++ deploy/zero_pad.sql | 22 +++ pkg/booking/public.go | 14 +- pkg/database/db.go | 2 +- pkg/database/types.go | 73 +++++++++ pkg/redsys/client.go | 191 +++++++----------------- po/ca.po | 64 ++++---- po/es.po | 65 ++++---- revert/encode_base64url.sql | 7 + revert/redsys.sql | 7 + revert/redsys_encrypt.sql | 7 + revert/redsys_environment.sql | 7 + revert/redsys_integration.sql | 7 + revert/redsys_request.sql | 7 + revert/redsys_sign_request.sql | 7 + revert/redsys_signed_request.sql | 7 + revert/setup_redsys.sql | 7 + revert/zero_pad.sql | 7 + sqitch.plan | 10 ++ test/currency.sql | 9 +- test/encode_base64url.sql | 48 ++++++ test/language.sql | 9 +- test/parse_price.sql | 2 +- test/redsys.sql | 245 +++++++++++++++++++++++++++++++ test/redsys_encrypt.sql | 49 +++++++ test/redsys_environment.sql | 18 +++ test/redsys_integration.sql | 18 +++ test/redsys_request.sql | 28 ++++ test/redsys_sign_request.sql | 49 +++++++ test/redsys_signed_request.sql | 22 +++ test/setup_redsys.sql | 78 ++++++++++ test/zero_pad.sql | 36 +++++ verify/available_currencies.sql | 2 + verify/available_languages.sql | 12 +- verify/currency.sql | 1 + verify/encode_base64url.sql | 7 + verify/language.sql | 1 + verify/redsys.sql | 20 +++ verify/redsys_encrypt.sql | 7 + verify/redsys_environment.sql | 7 + verify/redsys_integration.sql | 7 + verify/redsys_request.sql | 7 + verify/redsys_sign_request.sql | 7 + verify/redsys_signed_request.sql | 7 + verify/setup_redsys.sql | 7 + verify/zero_pad.sql | 7 + 63 files changed, 1303 insertions(+), 233 deletions(-) create mode 100644 deploy/encode_base64url.sql create mode 100644 deploy/redsys.sql create mode 100644 deploy/redsys_encrypt.sql create mode 100644 deploy/redsys_environment.sql create mode 100644 deploy/redsys_integration.sql create mode 100644 deploy/redsys_request.sql create mode 100644 deploy/redsys_sign_request.sql create mode 100644 deploy/redsys_signed_request.sql create mode 100644 deploy/setup_redsys.sql create mode 100644 deploy/zero_pad.sql create mode 100644 pkg/database/types.go create mode 100644 revert/encode_base64url.sql create mode 100644 revert/redsys.sql create mode 100644 revert/redsys_encrypt.sql create mode 100644 revert/redsys_environment.sql create mode 100644 revert/redsys_integration.sql create mode 100644 revert/redsys_request.sql create mode 100644 revert/redsys_sign_request.sql create mode 100644 revert/redsys_signed_request.sql create mode 100644 revert/setup_redsys.sql create mode 100644 revert/zero_pad.sql create mode 100644 test/encode_base64url.sql create mode 100644 test/redsys.sql create mode 100644 test/redsys_encrypt.sql create mode 100644 test/redsys_environment.sql create mode 100644 test/redsys_integration.sql create mode 100644 test/redsys_request.sql create mode 100644 test/redsys_sign_request.sql create mode 100644 test/redsys_signed_request.sql create mode 100644 test/setup_redsys.sql create mode 100644 test/zero_pad.sql create mode 100644 verify/encode_base64url.sql create mode 100644 verify/redsys.sql create mode 100644 verify/redsys_encrypt.sql create mode 100644 verify/redsys_environment.sql create mode 100644 verify/redsys_integration.sql create mode 100644 verify/redsys_request.sql create mode 100644 verify/redsys_sign_request.sql create mode 100644 verify/redsys_signed_request.sql create mode 100644 verify/setup_redsys.sql create mode 100644 verify/zero_pad.sql diff --git a/demo/demo.sql b/demo/demo.sql index bc20b63..12c4624 100644 --- a/demo/demo.sql +++ b/demo/demo.sql @@ -27,6 +27,8 @@ values (52, 42, 'employee') , (52, 43, 'admin') ; +select setup_redsys(52, '361716962', '1', 'test', 'redirect', 'sq7HjrUOBfKmC576ILgskD5srU870gJ7'); + alter table media alter column media_id restart with 62; select add_media(52, 'plots.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/plots.avif]])', 'base64')); select add_media(52, 'safari_tents.avif', 'image/avif', decode('m4_esyscmd([[base64 -w0 demo/safari_tents.avif]])', 'base64')); diff --git a/deploy/available_currencies.sql b/deploy/available_currencies.sql index 03a7e68..146ae04 100644 --- a/deploy/available_currencies.sql +++ b/deploy/available_currencies.sql @@ -4,9 +4,9 @@ begin; -insert into camper.currency(currency_code, currency_symbol) -values ('EUR', '€') - , ('USD', '$') +insert into camper.currency(currency_code, currency_symbol, redsys_code) +values ('EUR', '€', 978) + , ('USD', '$', 840) ; commit; diff --git a/deploy/available_languages.sql b/deploy/available_languages.sql index 50508b6..d5d860a 100644 --- a/deploy/available_languages.sql +++ b/deploy/available_languages.sql @@ -4,11 +4,11 @@ begin; -insert into public.language (lang_tag, name, endonym, selectable, currency_pattern) -values ('und', 'Undefined', 'Undefined', false, '%[3]s%.[1]*[2]f') - , ('ca', 'Catalan', 'català', true, '%.[1]*[2]f %[3]s') - , ('en', 'English', 'English', true, '%[3]s%.[1]*[2]f') - , ('es', 'Spanish', 'español', true, '%.[1]*[2]f %[3]s') +insert into public.language (lang_tag, name, endonym, selectable, currency_pattern, redsys_code) +values ('und', 'Undefined', 'Undefined', false, '%[3]s%.[1]*[2]f', 0) + , ('ca', 'Catalan', 'català', true, '%.[1]*[2]f %[3]s', 3) + , ('en', 'English', 'English', true, '%[3]s%.[1]*[2]f', 2) + , ('es', 'Spanish', 'español', true, '%.[1]*[2]f %[3]s', 1) ; commit; diff --git a/deploy/currency.sql b/deploy/currency.sql index dfb00b8..80d3dc4 100644 --- a/deploy/currency.sql +++ b/deploy/currency.sql @@ -10,9 +10,11 @@ set search_path to camper, public; create table currency ( currency_code currency_code primary key, currency_symbol text not null, - decimal_digits integer not null default 2 + decimal_digits integer not null default 2, + redsys_code integer not null -- Seem to be the same as ISO 4217 ); +grant select on table currency to guest; grant select on table currency to employee; grant select on table currency to admin; diff --git a/deploy/encode_base64url.sql b/deploy/encode_base64url.sql new file mode 100644 index 0000000..e061cac --- /dev/null +++ b/deploy/encode_base64url.sql @@ -0,0 +1,22 @@ +-- Deploy camper:encode_base64url to pg +-- requires: roles +-- requires: schema_camper + +begin; + +set search_path to camper, public; + +create or replace function encode_base64url(data bytea) returns text as +$$ + select translate(encode(data, 'base64'), E'+/\n', '-_'); +$$ + language sql + immutable +; + +revoke execute on function encode_base64url(bytea) from public; +grant execute on function encode_base64url(bytea) to guest; +grant execute on function encode_base64url(bytea) to employee; +grant execute on function encode_base64url(bytea) to admin; + +commit; diff --git a/deploy/encrypt_password.sql b/deploy/encrypt_password.sql index 3c4250f..28012e1 100644 --- a/deploy/encrypt_password.sql +++ b/deploy/encrypt_password.sql @@ -11,7 +11,7 @@ create or replace function encrypt_password() returns trigger as $$ begin if tg_op = 'INSERT' or new.password <> old.password then - new.password = crypt(new.password, gen_salt('bf')); + new.password = public.crypt(new.password, public.gen_salt('bf')); end if; return new; end; diff --git a/deploy/extension_pgcrypto.sql b/deploy/extension_pgcrypto.sql index 89aaacf..5331ede 100644 --- a/deploy/extension_pgcrypto.sql +++ b/deploy/extension_pgcrypto.sql @@ -3,6 +3,6 @@ begin; -create extension if not exists pgcrypto with schema auth; +create extension if not exists pgcrypto; commit; diff --git a/deploy/language.sql b/deploy/language.sql index 8bb76c8..f7e90c9 100644 --- a/deploy/language.sql +++ b/deploy/language.sql @@ -12,7 +12,8 @@ create table language name text not null, endonym text not null, selectable boolean not null, - currency_pattern text not null + currency_pattern text not null, + redsys_code integer not null ); grant select on table language to guest; diff --git a/deploy/login.sql b/deploy/login.sql index 99c18b7..2b203be 100644 --- a/deploy/login.sql +++ b/deploy/login.sql @@ -20,7 +20,7 @@ begin if not exists (select * from "user" where "user".email = login.email - and "user".password = crypt(login.password, "user".password)) then + and "user".password = public.crypt(login.password, "user".password)) then insert into login_attempt (user_name, ip_address, success) values (login.email, login.ip_address, false); return ''; @@ -34,7 +34,7 @@ begin and length(cookie) > 30; if user_cookie is null then - select encode(gen_random_bytes(25), 'hex') into user_cookie; + select encode(public.gen_random_bytes(25), 'hex') into user_cookie; end if; update "user" diff --git a/deploy/parse_price.sql b/deploy/parse_price.sql index f9d5fd6..ead0f75 100644 --- a/deploy/parse_price.sql +++ b/deploy/parse_price.sql @@ -51,6 +51,7 @@ comment on function parse_price(text, integer) is 'Converts the string representation of a price in decimal form to cents, according to the number of decimal digits passed.'; revoke execute on function parse_price(text, integer) from public; +grant execute on function parse_price(text, integer) to guest; grant execute on function parse_price(text, integer) to employee; grant execute on function parse_price(text, integer) to admin; diff --git a/deploy/redsys.sql b/deploy/redsys.sql new file mode 100644 index 0000000..75ee913 --- /dev/null +++ b/deploy/redsys.sql @@ -0,0 +1,61 @@ +-- Deploy camper:redsys to pg +-- requires: roles +-- requires: schema_camper +-- requires: company +-- requires: redsys_environment +-- requires: redsys_integration +-- requires: user_profile + +begin; + +set search_path to camper, public; + +create table redsys ( + company_id integer primary key references company, + merchant_code text not null constraint merchant_code_valid check (merchant_code ~ '^\d{9}$'), + terminal_number integer not null constraint terminal_number_in_range check(terminal_number > 0 and terminal_number < 1000), + environment redsys_environment not null, + integration redsys_integration not null, + encrypt_key bytea not null +); + +grant select (company_id, merchant_code, terminal_number, environment, integration) on table redsys to guest; +grant select (company_id, merchant_code, terminal_number, environment, integration) on table redsys to employee; +grant select (company_id, merchant_code, terminal_number, environment, integration) on table redsys to admin; +grant update (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) on table redsys to admin; +grant insert (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) on table redsys to admin; +grant delete on table redsys to admin; + +alter table redsys enable row level security; + +create policy guest_ok +on redsys +for select +using (true) +; + +create policy insert_to_company +on redsys +for insert +with check ( + company_id in (select company_id from user_profile) +) +; + +create policy update_company +on redsys +for update +using ( + company_id in (select company_id from user_profile) +) +; + +create policy delete_from_company +on redsys +for delete +using ( + company_id in (select company_id from user_profile) +) +; + +commit; diff --git a/deploy/redsys_encrypt.sql b/deploy/redsys_encrypt.sql new file mode 100644 index 0000000..91e2e3f --- /dev/null +++ b/deploy/redsys_encrypt.sql @@ -0,0 +1,29 @@ +-- Deploy camper:redsys_encrypt to pg +-- requires: roles +-- requires: schema_camper +-- requires: zero_pad +-- requires: redsys +-- requires: extension_pgcrypto + +begin; + +set search_path to camper, public; + +create or replace function redsys_encrypt(company_id integer, data bytea) returns bytea as +$$ + select public.encrypt_iv(zero_pad(data, 8), encrypt_key, '\x0000000000000000'::bytea, '3des-cbc/pad:none') + from redsys + where redsys.company_id = redsys_encrypt.company_id; +$$ + language sql + stable + security definer + set search_path = camper, pg_temp; +; + +revoke execute on function redsys_encrypt(integer, bytea) from public; +grant execute on function redsys_encrypt(integer, bytea) to guest; +grant execute on function redsys_encrypt(integer, bytea) to employee; +grant execute on function redsys_encrypt(integer, bytea) to admin; + +commit; diff --git a/deploy/redsys_environment.sql b/deploy/redsys_environment.sql new file mode 100644 index 0000000..caae000 --- /dev/null +++ b/deploy/redsys_environment.sql @@ -0,0 +1,10 @@ +-- Deploy camper:redsys_environment to pg +-- requires: schema_camper + +begin; + +set search_path to camper, public; + +create type redsys_environment as enum ('test', 'live'); + +commit; diff --git a/deploy/redsys_integration.sql b/deploy/redsys_integration.sql new file mode 100644 index 0000000..a771c4e --- /dev/null +++ b/deploy/redsys_integration.sql @@ -0,0 +1,13 @@ +-- Deploy camper:redsys_integration to pg +-- requires: schema_camper + +begin; + +set search_path to camper, public; + +create type redsys_integration as enum ( + 'insite', + 'redirect' +); + +commit; diff --git a/deploy/redsys_request.sql b/deploy/redsys_request.sql new file mode 100644 index 0000000..3a8fc9b --- /dev/null +++ b/deploy/redsys_request.sql @@ -0,0 +1,21 @@ +-- Deploy camper:redsys_request to pg +-- requires: schema_camper +-- requires: extension_uri + +begin; + +set search_path to camper, public; + +create type redsys_request as ( + transaction_type integer, + amount text, + order_number text, + product text, + card_holder text, + success_uri uri, + failure_uri uri, + notification_uri uri, + lang_tag text +); + +commit; diff --git a/deploy/redsys_sign_request.sql b/deploy/redsys_sign_request.sql new file mode 100644 index 0000000..8b2dd4e --- /dev/null +++ b/deploy/redsys_sign_request.sql @@ -0,0 +1,68 @@ +-- Deploy camper:redsys_sign_request to pg +-- requires: roles +-- requires: extension_pgcrypto +-- requires: schema_camper +-- requires: encode_base64url +-- requires: redsys_encrypt +-- requires: redsys_request +-- requires: redsys_signed_request +-- requires: company +-- requires: currency +-- requires: language + +begin; + +set search_path to camper, public; + +create or replace function redsys_sign_request(company_id integer, request redsys_request) returns redsys_signed_request as +$$ + select row( + merchant_parameters, + encode(hmac( + convert_to(merchant_parameters, 'UTF-8'), + redsys_encrypt(company_id, convert_to((request).order_number, 'UTF-8')), + 'sha256' + ), 'base64'), + 'HMAC_SHA256_V1' + ) + from ( + select encode_base64url( + convert_to( + -- not using JSONB because for unit test i need stable key order + json_build_object( + 'Ds_Merchant_MerchantCode', merchant_code, + 'Ds_Merchant_Terminal', terminal_number::text, + 'Ds_Merchant_MerchantName', business_name, + 'Ds_Merchant_TransactionType', (request).transaction_type::text, + 'Ds_Merchant_Amount', parse_price((request).amount, decimal_digits)::text, + 'Ds_Merchant_Currency', currency.redsys_code::text, + 'Ds_Merchant_Order', (request).order_number, + 'Ds_Merchant_Productdescription', (request).product, + 'Ds_Merchant_Titular', (request).card_holder, + 'Ds_Merchant_UrlOK', (request).success_uri::text, + 'Ds_Merchant_UrlKO', (request).failure_uri::text, + 'Ds_Merchant_MerchantURL', (request).notification_uri::text, + 'Ds_Merchant_ConsumerLanguage', language.redsys_code::text + )::text, + 'UTF-8' + ) + ) as merchant_parameters + from redsys + join company using (company_id) + join currency using (currency_code) + , language + where redsys.company_id = redsys_sign_request.company_id + and language.lang_tag = (request).lang_tag + ) as build + ; +$$ + language sql + stable +; + +revoke execute on function redsys_sign_request(integer, redsys_request) from public; +grant execute on function redsys_sign_request(integer, redsys_request) to guest; +grant execute on function redsys_sign_request(integer, redsys_request) to employee; +grant execute on function redsys_sign_request(integer, redsys_request) to admin; + +commit; diff --git a/deploy/redsys_signed_request.sql b/deploy/redsys_signed_request.sql new file mode 100644 index 0000000..b7ade69 --- /dev/null +++ b/deploy/redsys_signed_request.sql @@ -0,0 +1,14 @@ +-- Deploy camper:redsys_signed_request to pg +-- requires: schema_camper + +begin; + +set search_path to camper, public; + +create type redsys_signed_request as ( + merchant_parameters text, + signature text, + signature_version text +); + +commit; diff --git a/deploy/setup_redsys.sql b/deploy/setup_redsys.sql new file mode 100644 index 0000000..2b422e3 --- /dev/null +++ b/deploy/setup_redsys.sql @@ -0,0 +1,43 @@ +-- Deploy camper:setup_redsys to pg +-- requires: roles +-- requires: schema_camper +-- requires: redsys +-- requires: redsys_environment +-- requires: redsys_integration + +begin; + +set search_path to camper, public; + +create or replace function setup_redsys(company integer, merchant_code text, terminal_number integer, environment redsys_environment, integration redsys_integration, encrypt_key text) returns void as +$$ +begin + if encrypt_key is null then + update redsys + set merchant_code = setup_redsys.merchant_code, + terminal_number = setup_redsys.terminal_number, + environment = setup_redsys.environment, + integration = setup_redsys.integration, + encrypt_key = coalesce(decode(setup_redsys.encrypt_key, 'base64'), redsys.encrypt_key) + where company_id = company + ; + else + insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) + values (company, merchant_code, terminal_number, environment, integration, decode(encrypt_key, 'base64')) + on conflict (company_id) do update + set merchant_code = excluded.merchant_code, + terminal_number = excluded.terminal_number, + environment = excluded.environment, + integration = excluded.integration, + encrypt_key = coalesce(excluded.encrypt_key, redsys.encrypt_key) + ; + end if; +end +$$ + language plpgsql +; + +revoke execute on function setup_redsys(integer, text, integer, redsys_environment, redsys_integration, text) from public; +grant execute on function setup_redsys(integer, text, integer, redsys_environment, redsys_integration, text) to admin; + +commit; diff --git a/deploy/zero_pad.sql b/deploy/zero_pad.sql new file mode 100644 index 0000000..e1986d4 --- /dev/null +++ b/deploy/zero_pad.sql @@ -0,0 +1,22 @@ +-- Deploy camper:zero_pad to pg +-- requires: roles +-- requires: schema_camper + +begin; + +set search_path to camper, public; + +create or replace function zero_pad(data bytea, block_len integer) returns bytea as +$$ + select data || decode(lpad('', ((block_len - (octet_length(data) % block_len)) % block_len) * 2, '0'), 'hex'); +$$ + language sql + immutable +; + +revoke execute on function zero_pad(bytea, integer) from public; +grant execute on function zero_pad(bytea, integer) to guest; +grant execute on function zero_pad(bytea, integer) to employee; +grant execute on function zero_pad(bytea, integer) to admin; + +commit; diff --git a/pkg/booking/public.go b/pkg/booking/public.go index d127a24..c11c410 100644 --- a/pkg/booking/public.go +++ b/pkg/booking/public.go @@ -71,23 +71,19 @@ func makeReservation(w http.ResponseWriter, r *http.Request, user *auth.User, co return } - client, err := redsys.New("361716962", "001", "sq7HjrUOBfKmC576ILgskD5srU870gJ7") - if err != nil { - panic(err) - } schema := httplib.Protocol(r) authority := httplib.Host(r) request := &redsys.Request{ TransactionType: redsys.TransactionTypeCharge, - Amount: 1234, - Currency: redsys.CurrencyEUR, - Order: randomOrderNumber(), + Amount: "12.34", + OrderNumber: randomOrderNumber(), Product: "Test Booking", SuccessURL: fmt.Sprintf("%s://%s/%s/booking/success", schema, authority, user.Locale.Language), FailureURL: fmt.Sprintf("%s://%s/%s/booking/failure", schema, authority, user.Locale.Language), - ConsumerLanguage: redsys.LanguageCatalan, + NotificationURL: fmt.Sprintf("%s://%s/%s/booking/notification", schema, authority, user.Locale.Language), + ConsumerLanguage: user.Language, } - signed, err := client.SignRequest(request) + signed, err := redsys.SignRequest(r.Context(), conn, company, request) if err != nil { panic(err) } diff --git a/pkg/database/db.go b/pkg/database/db.go index 5eec52d..925dc71 100644 --- a/pkg/database/db.go +++ b/pkg/database/db.go @@ -29,7 +29,7 @@ func New(ctx context.Context, connString string) (*DB, error) { if _, err := conn.Exec(ctx, "set search_path to camper, public"); err != nil { return err } - return nil + return registerTypes(ctx, conn) } config.AfterRelease = func(conn *pgx.Conn) bool { diff --git a/pkg/database/types.go b/pkg/database/types.go new file mode 100644 index 0000000..a24998e --- /dev/null +++ b/pkg/database/types.go @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2023 jordi fita mas + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package database + +import ( + "context" + + "github.com/jackc/pgtype" + "github.com/jackc/pgx/v4" +) + +const ( + RedsysRequestTypeName = "redsys_request" + RedsysSignedRequestTypeName = "redsys_signed_request" +) + +func registerTypes(ctx context.Context, conn *pgx.Conn) error { + uriOID, err := registerType(ctx, conn, &pgtype.Text{}, "uri") + if err != nil { + return err + } + + redsysRequestType, err := pgtype.NewCompositeType( + RedsysRequestTypeName, + []pgtype.CompositeTypeField{ + {"transaction_type", pgtype.Int4OID}, + {"amount", pgtype.TextOID}, + {"order_number", pgtype.TextOID}, + {"product", pgtype.TextOID}, + {"card_holder", pgtype.TextOID}, + {"success_uri", uriOID}, + {"failure_uri", uriOID}, + {"notification_uri", uriOID}, + {"lang_tag", pgtype.TextOID}, + }, + conn.ConnInfo(), + ) + if err != nil { + return err + } + if _, err = registerType(ctx, conn, redsysRequestType, redsysRequestType.TypeName()); err != nil { + return err + } + + redsysSignedRequestType, err := pgtype.NewCompositeType( + RedsysSignedRequestTypeName, + []pgtype.CompositeTypeField{ + {"merchant_parameters", pgtype.TextOID}, + {"signature", pgtype.TextOID}, + {"signature_version", pgtype.TextOID}, + }, + conn.ConnInfo(), + ) + if err != nil { + return err + } + if _, err = registerType(ctx, conn, redsysSignedRequestType, redsysSignedRequestType.TypeName()); err != nil { + return err + } + + return nil +} + +func registerType(ctx context.Context, conn *pgx.Conn, value pgtype.Value, name string) (oid uint32, err error) { + if err = conn.QueryRow(ctx, "select $1::regtype::oid", name).Scan(&oid); err != nil { + return + } + conn.ConnInfo().RegisterDataType(pgtype.DataType{Value: value, Name: name, OID: oid}) + return +} diff --git a/pkg/redsys/client.go b/pkg/redsys/client.go index d5d69a5..ac86fd4 100644 --- a/pkg/redsys/client.go +++ b/pkg/redsys/client.go @@ -6,25 +6,14 @@ package redsys import ( - "bytes" - "crypto/cipher" - "crypto/des" - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "encoding/json" + "context" "fmt" - "strings" -) -type Language int + "github.com/jackc/pgtype" + "golang.org/x/text/language" -const ( - LanguageDefault Language = 0 - LanguageSpanish Language = 1 - LanguageEnglish Language = 2 - LanguageCatalan Language = 3 - LanguageFrench Language = 4 + "dev.tandem.ws/tandem/camper/pkg/auth" + "dev.tandem.ws/tandem/camper/pkg/database" ) type TransactionType int @@ -39,132 +28,16 @@ const ( TransactionTypeSplitConfirm TransactionType = 8 ) -type Currency int - -// Source: https://sis-d.redsys.es/pagosonline/codigos-autorizacion.html#monedas -// The codes seem to be the same as ISO 4217 https://en.wikipedia.org/wiki/ISO_4217 -const ( - CurrencyEUR Currency = 978 -) - type Request struct { TransactionType TransactionType - Amount int64 - Currency Currency - Order string + Amount string + OrderNumber string Product string CardHolder string SuccessURL string FailureURL string - ConsumerLanguage Language -} - -type merchantParameters struct { - MerchantCode string `json:"Ds_Merchant_MerchantCode"` - Terminal string `json:"Ds_Merchant_Terminal"` - TransactionType TransactionType `json:"Ds_Merchant_TransactionType,string"` - Amount int64 `json:"Ds_Merchant_Amount,string"` - Currency Currency `json:"Ds_Merchant_Currency,string"` - Order string `json:"Ds_Merchant_Order"` - MerchantURL string `json:"Ds_Merchant_MerchantURL,omitempty"` - Product string `json:"Ds_Merchant_ProductDescription,omitempty"` - CardHolder string `json:"Ds_Merchant_Titular,omitempty"` - SuccessURL string `json:"Ds_Merchant_UrlOK,omitempty"` - FailureURL string `json:"Ds_Merchant_UrlKO,omitempty"` - MerchantName string `json:"Ds_Merchant_MerchantName,omitempty"` - ConsumerLanguage Language `json:"Ds_Merchant_ConsumerLanguage,string,omitempty"` -} - -type Client struct { - merchantCode string - terminalCode string - crypto cipher.Block -} - -func New(merchantCode string, terminalCode string, key string) (*Client, error) { - b, err := base64.StdEncoding.DecodeString(key) - if err != nil { - return nil, fmt.Errorf("key is invalid base64: %v", err) - } - - crypto, err := des.NewTripleDESCipher(b) - if err != nil { - return nil, err - } - - return &Client{ - merchantCode: merchantCode, - terminalCode: terminalCode, - crypto: crypto, - }, nil -} - -func (p *Client) SignRequest(req *Request) (*SignedRequest, error) { - signed := &SignedRequest{ - SignatureVersion: "HMAC_SHA256_V1", - } - params := &merchantParameters{ - MerchantCode: p.merchantCode, - Terminal: p.terminalCode, - TransactionType: req.TransactionType, - Amount: req.Amount, - Currency: req.Currency, - Order: req.Order, - MerchantURL: "http://localhost/ca/", - Product: req.Product, - CardHolder: req.CardHolder, - SuccessURL: req.SuccessURL, - FailureURL: req.FailureURL, - MerchantName: "Merchant Here", - ConsumerLanguage: req.ConsumerLanguage, - } - var err error - signed.MerchantParameters, err = encodeMerchantParameters(params) - if err != nil { - return nil, err - } - signed.Signature = p.sign(params.Order, signed.MerchantParameters) - return signed, nil -} - -func encodeMerchantParameters(params *merchantParameters) (string, error) { - marshalled, err := json.Marshal(params) - if err != nil { - return "", err - } - return base64.URLEncoding.EncodeToString(marshalled), err -} - -func (p *Client) sign(id string, data string) string { - key := encrypt3DES(id, p.crypto) - return mac256(data, key) -} - -var iv = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - -func encrypt3DES(str string, crypto cipher.Block) string { - cbc := cipher.NewCBCEncrypter(crypto, iv) - - decrypted := []byte(str) - decryptedPadded, _ := zeroPad(decrypted, crypto.BlockSize()) - cbc.CryptBlocks(decryptedPadded, decryptedPadded) - - return base64.StdEncoding.EncodeToString(decryptedPadded) -} - -func zeroPad(data []byte, blockLen int) ([]byte, error) { - padLen := (blockLen - (len(data) % blockLen)) % blockLen - pad := bytes.Repeat([]byte{0x00}, padLen) - - return append(data, pad...), nil -} - -func mac256(data string, key string) string { - decodedKey, _ := base64.StdEncoding.DecodeString(key) - res := hmac.New(sha256.New, decodedKey) - res.Write([]byte(strings.TrimSpace(data))) - result := res.Sum(nil) - return base64.StdEncoding.EncodeToString(result) + NotificationURL string + ConsumerLanguage language.Tag } type SignedRequest struct { @@ -172,3 +45,49 @@ type SignedRequest struct { Signature string SignatureVersion string } + +func SignRequest(ctx context.Context, conn *database.Conn, company *auth.Company, req *Request) (*SignedRequest, error) { + row := conn.QueryRow(ctx, "select redsys_sign_request($1, $2)", company.ID, req) + signed := &SignedRequest{} + if err := row.Scan(&signed); err != nil { + return nil, err + } + return signed, nil +} + +func (src Request) EncodeText(ci *pgtype.ConnInfo, dst []byte) ([]byte, error) { + typeName := database.RedsysRequestTypeName + dt, ok := ci.DataTypeForName(typeName) + if !ok { + return nil, fmt.Errorf("unable to find oid for type name %v", typeName) + } + values := []interface{}{ + src.TransactionType, + src.Amount, + src.OrderNumber, + src.Product, + src.CardHolder, + src.SuccessURL, + src.FailureURL, + src.NotificationURL, + src.ConsumerLanguage, + } + ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType) + if err := ct.Set(values); err != nil { + return nil, err + } + return ct.EncodeText(ci, dst) +} + +func (dst *SignedRequest) DecodeText(ci *pgtype.ConnInfo, src []byte) error { + typeName := database.RedsysSignedRequestTypeName + dt, ok := ci.DataTypeForName(typeName) + if !ok { + return fmt.Errorf("unable to find oid for type name %v", typeName) + } + ct := pgtype.NewValue(dt.Value).(*pgtype.CompositeType) + if err := ct.DecodeText(ci, src); err != nil { + return err + } + return ct.AssignTo(dst) +} diff --git a/po/ca.po b/po/ca.po index a2a0b7f..fa7ba25 100644 --- a/po/ca.po +++ b/po/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-10-19 21:23+0200\n" +"POT-Creation-Date: 2023-10-27 01:12+0200\n" "PO-Revision-Date: 2023-07-22 23:45+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Catalan \n" @@ -1211,12 +1211,12 @@ msgid "Slide image must be an image media type." msgstr "La imatge de la diapositiva ha de ser un mèdia de tipus imatge." #: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:210 -#: pkg/booking/public.go:292 +#: pkg/booking/public.go:288 msgid "Email can not be empty." msgstr "No podeu deixar el correu-e en blanc." #: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:211 -#: pkg/booking/public.go:293 +#: pkg/booking/public.go:289 msgid "This email is not valid. It should be like name@domain.com." msgstr "Aquest correu-e no és vàlid. Hauria de ser similar a nom@domini.com." @@ -1349,7 +1349,7 @@ msgctxt "season" msgid "Closed" msgstr "Tancat" -#: pkg/campsite/admin.go:226 pkg/booking/public.go:321 +#: pkg/campsite/admin.go:226 pkg/booking/public.go:317 msgid "Selected campsite type is not valid." msgstr "El tipus d’allotjament escollit no és vàlid." @@ -1446,7 +1446,7 @@ msgstr "No podeu deixar la data de fi en blanc." msgid "End date must be a valid date." msgstr "La data de fi ha de ser una data vàlida." -#: pkg/company/admin.go:193 pkg/booking/public.go:279 +#: pkg/company/admin.go:193 pkg/booking/public.go:275 msgid "Selected country is not valid." msgstr "El país escollit no és vàlid." @@ -1466,11 +1466,11 @@ msgstr "No podeu deixar el NIF en blanc." msgid "This VAT number is not valid." msgstr "Aquest NIF no és vàlid." -#: pkg/company/admin.go:205 pkg/booking/public.go:295 +#: pkg/company/admin.go:205 pkg/booking/public.go:291 msgid "Phone can not be empty." msgstr "No podeu deixar el telèfon en blanc." -#: pkg/company/admin.go:206 pkg/booking/public.go:296 +#: pkg/company/admin.go:206 pkg/booking/public.go:292 msgid "This phone number is not valid." msgstr "Aquest número de telèfon no és vàlid." @@ -1494,7 +1494,7 @@ msgstr "No podeu deixar la província en blanc." msgid "Postal code can not be empty." msgstr "No podeu deixar el codi postal en blanc." -#: pkg/company/admin.go:220 pkg/booking/public.go:288 +#: pkg/company/admin.go:220 pkg/booking/public.go:284 msgid "This postal code is not valid." msgstr "Aquest codi postal no és vàlid." @@ -1522,102 +1522,102 @@ msgstr "No podeu deixar el fitxer del mèdia en blanc." msgid "Filename can not be empty." msgstr "No podeu deixar el nom del fitxer en blanc." -#: pkg/booking/public.go:283 +#: pkg/booking/public.go:279 msgid "Full name can not be empty." msgstr "No podeu deixar el nom i els cognoms en blanc." -#: pkg/booking/public.go:284 +#: pkg/booking/public.go:280 msgid "Full name must have at least one letter." msgstr "El nom i els cognoms han de tenir com a mínim una lletra." -#: pkg/booking/public.go:300 +#: pkg/booking/public.go:296 msgid "Number of adults can not be empty" msgstr "No podeu deixar el número d’adults en blanc." -#: pkg/booking/public.go:301 +#: pkg/booking/public.go:297 msgid "Number of adults must be an integer." msgstr "El número d’adults ha de ser enter." -#: pkg/booking/public.go:302 +#: pkg/booking/public.go:298 msgid "Number of adults must be one or greater." msgstr "El número d’adults no pot ser zero." -#: pkg/booking/public.go:305 +#: pkg/booking/public.go:301 msgid "Number of teenagers can not be empty" msgstr "No podeu deixar el número d’adolescents en blanc." -#: pkg/booking/public.go:306 +#: pkg/booking/public.go:302 msgid "Number of teenagers must be an integer." msgstr "El número d’adolescents ha de ser enter." -#: pkg/booking/public.go:307 +#: pkg/booking/public.go:303 msgid "Number of teenagers must be zero or greater." msgstr "El número d’adolescents ha de ser com a mínim zero." -#: pkg/booking/public.go:310 +#: pkg/booking/public.go:306 msgid "Number of children can not be empty" msgstr "No podeu deixar el número de nens en blanc." -#: pkg/booking/public.go:311 +#: pkg/booking/public.go:307 msgid "Number of children must be an integer." msgstr "El número de nens ha de ser enter." -#: pkg/booking/public.go:312 +#: pkg/booking/public.go:308 msgid "Number of children must be zero or greater." msgstr "El número de nens ha de ser com a mínim zero." -#: pkg/booking/public.go:315 +#: pkg/booking/public.go:311 msgid "Number of dogs can not be empty" msgstr "No podeu deixar el número de gossos en blanc." -#: pkg/booking/public.go:316 +#: pkg/booking/public.go:312 msgid "Number of dogs must be an integer." msgstr "El número de gossos ha de ser enter." -#: pkg/booking/public.go:317 +#: pkg/booking/public.go:313 msgid "Number of dogs must be zero or greater." msgstr "El número de gossos ha de ser com a mínim zero." -#: pkg/booking/public.go:322 +#: pkg/booking/public.go:318 msgid "Arrival date can not be empty" msgstr "No podeu deixar la data d’arribada en blanc." -#: pkg/booking/public.go:323 +#: pkg/booking/public.go:319 msgid "Arrival date must be a valid date." msgstr "La data d’arribada ha de ser una data vàlida." -#: pkg/booking/public.go:327 +#: pkg/booking/public.go:323 msgid "Departure date can not be empty" msgstr "No podeu deixar la data de sortida en blanc." -#: pkg/booking/public.go:328 +#: pkg/booking/public.go:324 msgid "Departure date must be a valid date." msgstr "La data de sortida ha de ser una data vàlida." -#: pkg/booking/public.go:329 +#: pkg/booking/public.go:325 msgid "The departure date must be after the arrival date." msgstr "La data de sortida ha de ser posterior a la d’arribada." -#: pkg/booking/public.go:332 +#: pkg/booking/public.go:328 msgid "It is mandatory to agree to the reservation conditions." msgstr "És obligatori acceptar les condicions de reserves." -#: pkg/booking/public.go:335 +#: pkg/booking/public.go:331 #, c-format msgid "%s can not be empty" msgstr "No podeu deixar %s en blanc." -#: pkg/booking/public.go:336 +#: pkg/booking/public.go:332 #, c-format msgid "%s must be an integer." msgstr "%s ha de ser un número enter." -#: pkg/booking/public.go:337 +#: pkg/booking/public.go:333 #, c-format msgid "%s must be %d or greater." msgstr "El valor de %s ha de ser com a mínim %d." -#: pkg/booking/public.go:338 +#: pkg/booking/public.go:334 #, c-format msgid "%s must be at most %d." msgstr "El valor de %s ha de ser com a màxim %d." diff --git a/po/es.po b/po/es.po index 089ceb6..6fdbae1 100644 --- a/po/es.po +++ b/po/es.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: camper\n" "Report-Msgid-Bugs-To: jordi@tandem.blog\n" -"POT-Creation-Date: 2023-10-19 21:23+0200\n" +"POT-Creation-Date: 2023-10-27 01:12+0200\n" "PO-Revision-Date: 2023-07-22 23:46+0200\n" "Last-Translator: jordi fita mas \n" "Language-Team: Spanish \n" @@ -1211,12 +1211,12 @@ msgid "Slide image must be an image media type." msgstr "La imagen de la diapositiva tiene que ser un medio de tipo imagen." #: pkg/app/login.go:56 pkg/app/user.go:246 pkg/company/admin.go:210 -#: pkg/booking/public.go:292 +#: pkg/booking/public.go:288 msgid "Email can not be empty." msgstr "No podéis dejar el correo-e en blanco." #: pkg/app/login.go:57 pkg/app/user.go:247 pkg/company/admin.go:211 -#: pkg/booking/public.go:293 +#: pkg/booking/public.go:289 msgid "This email is not valid. It should be like name@domain.com." msgstr "Este correo-e no es válido. Tiene que ser parecido a nombre@dominio.com." @@ -1349,7 +1349,7 @@ msgctxt "season" msgid "Closed" msgstr "Cerrado" -#: pkg/campsite/admin.go:226 pkg/booking/public.go:321 +#: pkg/campsite/admin.go:226 pkg/booking/public.go:317 msgid "Selected campsite type is not valid." msgstr "El tipo de alojamiento escogido no es válido." @@ -1446,7 +1446,7 @@ msgstr "No podéis dejar la fecha final en blanco." msgid "End date must be a valid date." msgstr "La fecha final tiene que ser una fecha válida." -#: pkg/company/admin.go:193 pkg/booking/public.go:279 +#: pkg/company/admin.go:193 pkg/booking/public.go:275 msgid "Selected country is not valid." msgstr "El país escogido no es válido." @@ -1466,11 +1466,11 @@ msgstr "No podéis dejar el NIF en blanco." msgid "This VAT number is not valid." msgstr "Este NIF no es válido." -#: pkg/company/admin.go:205 pkg/booking/public.go:295 +#: pkg/company/admin.go:205 pkg/booking/public.go:291 msgid "Phone can not be empty." msgstr "No podéis dejar el teléfono en blanco." -#: pkg/company/admin.go:206 pkg/booking/public.go:296 +#: pkg/company/admin.go:206 pkg/booking/public.go:292 msgid "This phone number is not valid." msgstr "Este teléfono no es válido." @@ -1494,7 +1494,7 @@ msgstr "No podéis dejar la provincia en blanco." msgid "Postal code can not be empty." msgstr "No podéis dejar el código postal en blanco." -#: pkg/company/admin.go:220 pkg/booking/public.go:288 +#: pkg/company/admin.go:220 pkg/booking/public.go:284 msgid "This postal code is not valid." msgstr "Este código postal no es válido." @@ -1522,101 +1522,102 @@ msgstr "No podéis dejar el archivo del medio en blanco." msgid "Filename can not be empty." msgstr "No podéis dejar el nombre del archivo en blanco." -#: pkg/booking/public.go:283 +#: pkg/booking/public.go:279 msgid "Full name can not be empty." msgstr "No podéis dejar el nombre y los apellidos en blanco." -#: pkg/booking/public.go:284 +#: pkg/booking/public.go:280 msgid "Full name must have at least one letter." msgstr "El nombre y los apellidos tienen que tener como mínimo una letra." -#: pkg/booking/public.go:300 +#: pkg/booking/public.go:296 msgid "Number of adults can not be empty" msgstr "No podéis dejar el número de adultos blanco." -#: pkg/booking/public.go:301 +#: pkg/booking/public.go:297 msgid "Number of adults must be an integer." msgstr "El número de adultos tiene que ser entero." -#: pkg/booking/public.go:302 +#: pkg/booking/public.go:298 msgid "Number of adults must be one or greater." msgstr "El número de adultos no puede ser cero." -#: pkg/booking/public.go:305 +#: pkg/booking/public.go:301 msgid "Number of teenagers can not be empty" msgstr "No podéis dejar el número de adolescentes en blanco." -#: pkg/booking/public.go:306 +#: pkg/booking/public.go:302 msgid "Number of teenagers must be an integer." msgstr "El número de adolescentes tiene que ser entero." -#: pkg/booking/public.go:307 +#: pkg/booking/public.go:303 msgid "Number of teenagers must be zero or greater." msgstr "El número de adolescentes tiene que ser como mínimo cero." -#: pkg/booking/public.go:310 +#: pkg/booking/public.go:306 msgid "Number of children can not be empty" msgstr "No podéis dejar el número de niños en blanco." -#: pkg/booking/public.go:311 +#: pkg/booking/public.go:307 msgid "Number of children must be an integer." msgstr "El número de niños tiene que ser entero." -#: pkg/booking/public.go:312 +#: pkg/booking/public.go:308 msgid "Number of children must be zero or greater." msgstr "El número de niños tiene que ser como mínimo cero." -#: pkg/booking/public.go:315 +#: pkg/booking/public.go:311 msgid "Number of dogs can not be empty" msgstr "No podéis dejar el número de perros en blanco." -#: pkg/booking/public.go:316 +#: pkg/booking/public.go:312 msgid "Number of dogs must be an integer." msgstr "El número de perros tiene que ser entero." -#: pkg/booking/public.go:317 +#: pkg/booking/public.go:313 msgid "Number of dogs must be zero or greater." msgstr "El número de perros tiene que ser como mínimo cero." -#: pkg/booking/public.go:322 +#: pkg/booking/public.go:318 msgid "Arrival date can not be empty" msgstr "No podéis dejar la fecha de llegada en blanco." -#: pkg/booking/public.go:323 +#: pkg/booking/public.go:319 msgid "Arrival date must be a valid date." msgstr "La fecha de llegada tiene que ser una fecha válida." -#: pkg/booking/public.go:327 +#: pkg/booking/public.go:323 msgid "Departure date can not be empty" msgstr "No podéis dejar la fecha de partida en blanco." -#: pkg/booking/public.go:328 +#: pkg/booking/public.go:324 msgid "Departure date must be a valid date." msgstr "La fecha de partida tiene que ser una fecha válida." -#: pkg/booking/public.go:329 +#: pkg/booking/public.go:325 msgid "The departure date must be after the arrival date." msgstr "La fecha de partida tiene que ser posterior a la de llegada." -#: pkg/booking/public.go:332 +#: pkg/booking/public.go:328 msgid "It is mandatory to agree to the reservation conditions." msgstr "Es obligatorio aceptar las condiciones de reserva." -#: pkg/booking/public.go:335 +#: pkg/booking/public.go:331 +#, c-format msgid "%s can not be empty" msgstr "No podéis dejar %s en blanco." -#: pkg/booking/public.go:336 +#: pkg/booking/public.go:332 #, c-format msgid "%s must be an integer." msgstr "%s tiene que ser un número entero." -#: pkg/booking/public.go:337 +#: pkg/booking/public.go:333 #, c-format msgid "%s must be %d or greater." msgstr "%s tiene que ser como mínimo %d." -#: pkg/booking/public.go:338 +#: pkg/booking/public.go:334 #, c-format msgid "%s must be at most %d." msgstr "%s tiene que ser como máximo %d" diff --git a/revert/encode_base64url.sql b/revert/encode_base64url.sql new file mode 100644 index 0000000..3fb5505 --- /dev/null +++ b/revert/encode_base64url.sql @@ -0,0 +1,7 @@ +-- Revert camper:encode_base64url from pg + +begin; + +drop function if exists camper.encode_base64url(bytea); + +commit; diff --git a/revert/redsys.sql b/revert/redsys.sql new file mode 100644 index 0000000..42e7904 --- /dev/null +++ b/revert/redsys.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys from pg + +begin; + +drop table if exists camper.redsys; + +commit; diff --git a/revert/redsys_encrypt.sql b/revert/redsys_encrypt.sql new file mode 100644 index 0000000..a43469f --- /dev/null +++ b/revert/redsys_encrypt.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_encrypt from pg + +begin; + +drop function if exists camper.redsys_encrypt(integer, bytea); + +commit; diff --git a/revert/redsys_environment.sql b/revert/redsys_environment.sql new file mode 100644 index 0000000..57365d4 --- /dev/null +++ b/revert/redsys_environment.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_environment from pg + +begin; + +drop type if exists camper.redsys_environment; + +commit; diff --git a/revert/redsys_integration.sql b/revert/redsys_integration.sql new file mode 100644 index 0000000..9330a7e --- /dev/null +++ b/revert/redsys_integration.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_integration from pg + +begin; + +drop type if exists camper.redsys_integration; + +commit; diff --git a/revert/redsys_request.sql b/revert/redsys_request.sql new file mode 100644 index 0000000..c2c0462 --- /dev/null +++ b/revert/redsys_request.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_request from pg + +begin; + +drop type if exists camper.redsys_request; + +commit; diff --git a/revert/redsys_sign_request.sql b/revert/redsys_sign_request.sql new file mode 100644 index 0000000..0021d65 --- /dev/null +++ b/revert/redsys_sign_request.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_sign_request from pg + +begin; + +drop function if exists camper.redsys_sign_request(integer, camper.redsys_request); + +commit; diff --git a/revert/redsys_signed_request.sql b/revert/redsys_signed_request.sql new file mode 100644 index 0000000..c152a04 --- /dev/null +++ b/revert/redsys_signed_request.sql @@ -0,0 +1,7 @@ +-- Revert camper:redsys_signed_request from pg + +begin; + +drop type if exists camper.redsys_signed_request; + +commit; diff --git a/revert/setup_redsys.sql b/revert/setup_redsys.sql new file mode 100644 index 0000000..cfb7cee --- /dev/null +++ b/revert/setup_redsys.sql @@ -0,0 +1,7 @@ +-- Revert camper:setup_redsys from pg + +begin; + +drop function if exists camper.setup_redsys(integer, text, integer, camper.redsys_environment, camper.redsys_integration, text); + +commit; diff --git a/revert/zero_pad.sql b/revert/zero_pad.sql new file mode 100644 index 0000000..82f45f1 --- /dev/null +++ b/revert/zero_pad.sql @@ -0,0 +1,7 @@ +-- Revert camper:zero_pad from pg + +begin; + +drop function if exists camper.zero_pad(bytea, integer); + +commit; diff --git a/sqitch.plan b/sqitch.plan index 808bc9c..c7c1b7c 100644 --- a/sqitch.plan +++ b/sqitch.plan @@ -109,3 +109,13 @@ campsite_type_feature_i18n [roles schema_camper campsite_type_feature language] add_campsite_type_feature [roles schema_camper campsite_type_feature campsite_type] 2023-10-13T16:26:04Z jordi fita mas # Add function to create new campsite type features edit_campsite_type_feature [roles schema_camper campsite_type_feature] 2023-10-13T16:37:43Z jordi fita mas # Add function to update campsite type features translate_campsite_type_feature [roles schema_camper campsite_type_feature_i18n] 2023-10-13T16:43:55Z jordi fita mas # Add function to translate campsite type features +redsys_environment [schema_camper] 2023-10-26T17:55:13Z jordi fita mas # Add enumeration of Redsys environments +redsys_integration [schema_camper] 2023-10-26T18:10:21Z jordi fita mas # Add enumeration for Redsys integrations +redsys [roles schema_camper company redsys_environment redsys_integration user_profile] 2023-10-26T16:26:23Z jordi fita mas # Add the relation for Redsys settings +setup_redsys [roles schema_camper redsys redsys_environment redsys_integration] 2023-10-26T17:20:29Z jordi fita mas # Add function to setup Redsys parameters +redsys_request [schema_camper extension_uri] 2023-10-26T18:50:55Z jordi fita mas # Add the composite type of a Redsys request +redsys_signed_request [schema_camper] 2023-10-26T18:54:48Z jordi fita mas # Add the composite type of a Redsys signed request +zero_pad [roles schema_camper] 2023-10-26T19:57:00Z jordi fita mas # Add function to pad a bytea with zeros in front +redsys_encrypt [roles schema_camper zero_pad redsys extension_pgcrypto] 2023-10-26T20:18:00Z jordi fita mas # Add function to encrypt data with Redsys’ encrypt key +encode_base64url [roles schema_camper] 2023-10-26T21:00:47Z jordi fita mas # Add function to encode bytea the so-called base64url +redsys_sign_request [roles extension_pgcrypto schema_camper encode_base64url redsys_encrypt redsys_request redsys_signed_request company currency language] 2023-10-26T21:12:01Z jordi fita mas # Add the function that signs Redsys requests diff --git a/test/currency.sql b/test/currency.sql index 73b735a..e8162e1 100644 --- a/test/currency.sql +++ b/test/currency.sql @@ -5,13 +5,13 @@ reset client_min_messages; begin; -select plan(20); +select plan(24); set search_path to camper, public; select has_table('currency'); select has_pk('currency' ); -select table_privs_are('currency', 'guest', array []::text[]); +select table_privs_are('currency', 'guest', array ['SELECT']); select table_privs_are('currency', 'employee', array ['SELECT']); select table_privs_are('currency', 'admin', array ['SELECT']); select table_privs_are('currency', 'authenticator', array []::text[]); @@ -33,6 +33,11 @@ select col_not_null('currency', 'decimal_digits'); select col_has_default('currency', 'decimal_digits'); select col_default_is('currency', 'decimal_digits', 2); +select has_column('currency', 'redsys_code'); +select col_type_is('currency', 'redsys_code', 'integer'); +select col_not_null('currency', 'redsys_code'); +select col_hasnt_default('currency', 'redsys_code'); + select * from finish(); diff --git a/test/encode_base64url.sql b/test/encode_base64url.sql new file mode 100644 index 0000000..179b5da --- /dev/null +++ b/test/encode_base64url.sql @@ -0,0 +1,48 @@ +-- Test encode_base64url +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(14); + +set search_path to camper, public; + +select has_function('camper', 'encode_base64url', array['bytea']); +select function_lang_is('camper', 'encode_base64url', array['bytea'], 'sql'); +select function_returns('camper', 'encode_base64url', array['bytea'], 'text'); +select isnt_definer('camper', 'encode_base64url', array['bytea']); +select volatility_is('camper', 'encode_base64url', array['bytea'], 'immutable'); +select function_privs_are('camper', 'encode_base64url', array ['bytea'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'encode_base64url', array ['bytea'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'encode_base64url', array ['bytea'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'encode_base64url', array ['bytea'], 'authenticator', array[]::text[]); + +select is( + encode_base64url(decode('sq7HjrUOBfKmC576ILgskD5srU870gJ7', 'base64')), + 'sq7HjrUOBfKmC576ILgskD5srU870gJ7' +); +select is( + encode_base64url(decode('K+gWWTl+gKlkxEGDsuc0UunIQwoFLM0t', 'base64')), + 'K-gWWTl-gKlkxEGDsuc0UunIQwoFLM0t' +); +select is( + encode_base64url(decode('cQGdqnb4V/nSKe8zTzyP/VaDGd2rAmzt', 'base64')), + 'cQGdqnb4V_nSKe8zTzyP_VaDGd2rAmzt' +); +select is( + encode_base64url(decode('x59e+kbXSpgsmy31V5l2BtTuKv8/pVKg', 'base64')), + 'x59e-kbXSpgsmy31V5l2BtTuKv8_pVKg' +); + +select is( + encode_base64url(decode(E'yvmzOBJpRfBn2XodxH/1WOpbASfcH5KAMRbDNevBV5srR8Yj5wAiP6+Obv0NCpyrOUVeYZ8NqMe/\ntM28EoOY2g==', 'base64')), + 'yvmzOBJpRfBn2XodxH_1WOpbASfcH5KAMRbDNevBV5srR8Yj5wAiP6-Obv0NCpyrOUVeYZ8NqMe_tM28EoOY2g==', + 'Should not include newlines in the output' +); + +select * +from finish(); + +rollback; diff --git a/test/language.sql b/test/language.sql index a53ab09..af57ae0 100644 --- a/test/language.sql +++ b/test/language.sql @@ -7,14 +7,14 @@ begin; set search_path to public; -select plan(27); +select plan(31); select has_table('language'); select has_pk('language'); select table_privs_are('language', 'guest', array ['SELECT']); select table_privs_are('language', 'employee', array ['SELECT']); select table_privs_are('language', 'admin', array ['SELECT']); -select table_privs_are('language', 'authenticator', array ['SELECT']::text[]); +select table_privs_are('language', 'authenticator', array ['SELECT']); select has_column('language', 'lang_tag'); select col_is_pk('language', 'lang_tag'); @@ -42,6 +42,11 @@ select col_type_is('language', 'currency_pattern', 'text'); select col_not_null('language', 'currency_pattern'); select col_hasnt_default('language', 'currency_pattern'); +select has_column('language', 'redsys_code'); +select col_type_is('language', 'redsys_code', 'integer'); +select col_not_null('language', 'redsys_code'); +select col_hasnt_default('language', 'redsys_code'); + select * from finish(); diff --git a/test/parse_price.sql b/test/parse_price.sql index ed5090c..4e21e45 100644 --- a/test/parse_price.sql +++ b/test/parse_price.sql @@ -14,7 +14,7 @@ select function_lang_is('camper', 'parse_price', array ['text', 'integer'], 'plp select function_returns('camper', 'parse_price', array ['text', 'integer'], 'integer'); select isnt_definer('camper', 'parse_price', array ['text', 'integer']); select volatility_is('camper', 'parse_price', array ['text', 'integer'], 'immutable'); -select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'guest', array []::text[]); +select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'guest', array ['EXECUTE']); select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'employee', array ['EXECUTE']); select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'admin', array ['EXECUTE']); select function_privs_are('camper', 'parse_price', array ['text', 'integer'], 'authenticator', array []::text[]); diff --git a/test/redsys.sql b/test/redsys.sql new file mode 100644 index 0000000..b983205 --- /dev/null +++ b/test/redsys.sql @@ -0,0 +1,245 @@ +-- Test redsys +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(75); + +set search_path to camper, public; + +select has_table('redsys'); +select has_pk('redsys'); +select table_privs_are('redsys', 'guest', array []::text[]); +select table_privs_are('redsys', 'employee', array []::text[]); +select table_privs_are('redsys', 'admin', array ['DELETE']); +select table_privs_are('redsys', 'authenticator', array []::text[]); + +select has_column('redsys', 'company_id'); +select col_is_pk('redsys', 'company_id'); +select col_is_fk('redsys', 'company_id'); +select fk_ok('redsys', 'company_id', 'company', 'company_id'); +select col_type_is('redsys', 'company_id', 'integer'); +select col_not_null('redsys', 'company_id'); +select col_hasnt_default('redsys', 'company_id'); +select column_privs_are('redsys', 'company_id', 'guest', array ['SELECT']); +select column_privs_are('redsys', 'company_id', 'employee', array ['SELECT']); +select column_privs_are('redsys', 'company_id', 'admin', array ['SELECT', 'INSERT', 'UPDATE']); +select column_privs_are('redsys', 'company_id', 'authenticator', array []::text[]); + +select has_column('redsys', 'merchant_code'); +select col_type_is('redsys', 'merchant_code', 'text'); +select col_not_null('redsys', 'merchant_code'); +select col_hasnt_default('redsys', 'merchant_code'); +select column_privs_are('redsys', 'merchant_code', 'guest', array ['SELECT']); +select column_privs_are('redsys', 'merchant_code', 'employee', array ['SELECT']); +select column_privs_are('redsys', 'merchant_code', 'admin', array ['SELECT', 'INSERT', 'UPDATE']); +select column_privs_are('redsys', 'merchant_code', 'authenticator', array []::text[]); + +select has_column('redsys', 'terminal_number'); +select col_type_is('redsys', 'terminal_number', 'integer'); +select col_not_null('redsys', 'terminal_number'); +select col_hasnt_default('redsys', 'terminal_number'); +select column_privs_are('redsys', 'terminal_number', 'guest', array ['SELECT']); +select column_privs_are('redsys', 'terminal_number', 'employee', array ['SELECT']); +select column_privs_are('redsys', 'terminal_number', 'admin', array ['SELECT', 'INSERT', 'UPDATE']); +select column_privs_are('redsys', 'terminal_number', 'authenticator', array []::text[]); + +select has_column('redsys', 'environment'); +select col_type_is('redsys', 'environment', 'redsys_environment'); +select col_not_null('redsys', 'environment'); +select col_hasnt_default('redsys', 'environment'); +select column_privs_are('redsys', 'environment', 'guest', array ['SELECT']); +select column_privs_are('redsys', 'environment', 'employee', array ['SELECT']); +select column_privs_are('redsys', 'environment', 'admin', array ['SELECT', 'INSERT', 'UPDATE']); +select column_privs_are('redsys', 'environment', 'authenticator', array []::text[]); + +select has_column('redsys', 'integration'); +select col_type_is('redsys', 'integration', 'redsys_integration'); +select col_not_null('redsys', 'integration'); +select col_hasnt_default('redsys', 'integration'); +select column_privs_are('redsys', 'integration', 'guest', array ['SELECT']); +select column_privs_are('redsys', 'integration', 'employee', array ['SELECT']); +select column_privs_are('redsys', 'integration', 'admin', array ['SELECT', 'INSERT', 'UPDATE']); +select column_privs_are('redsys', 'integration', 'authenticator', array []::text[]); + +select has_column('redsys', 'encrypt_key'); +select col_type_is('redsys', 'encrypt_key', 'bytea'); +select col_not_null('redsys', 'encrypt_key'); +select col_hasnt_default('redsys', 'encrypt_key'); +select column_privs_are('redsys', 'encrypt_key', 'guest', array []::text[]); +select column_privs_are('redsys', 'encrypt_key', 'employee', array []::text[]); +select column_privs_are('redsys', 'encrypt_key', 'admin', array ['INSERT', 'UPDATE']); +select column_privs_are('redsys', 'encrypt_key', 'authenticator', array []::text[]); + + +set client_min_messages to warning; +truncate redsys cascade; +truncate company_host cascade; +truncate company_user cascade; +truncate company cascade; +truncate auth."user" cascade; +reset client_min_messages; + + +insert into auth."user" (user_id, email, name, password, cookie, cookie_expires_at) +values (1, 'demo@tandem.blog', 'Demo', 'test', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month') + , (5, 'admin@tandem.blog', 'Demo', 'test', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month') +; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, country_code, currency_code, default_lang_tag) +values ( 2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 'ES', 'EUR', 'ca') + , ( 4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 'FR', 'USD', 'ca') + , ( 6, 'Company 6', 'XX345', '', '777-777-777', 'c@c', '', '', '', '', '', '', 'GB', 'EUR', 'en') + , ( 8, 'Company 8', 'XX456', '', '888-888-888', 'd@d', '', '', '', '', '', '', 'DE', 'USD', 'es') +; + +insert into company_user (company_id, user_id, role) +values (2, 1, 'admin') + , (4, 5, 'admin') +; + +insert into company_host (company_id, host) +values (2, 'co2') + , (4, 'co4') +; + +insert into redsys(company_id, merchant_code, terminal_number, environment, integration, encrypt_key) +values (4, '444444444', 4, 'live', 'insite', E'\\x44') + , (8, '888888888', 8, 'test', 'redirect', E'\\x88') +; + +prepare redsys_data as +select company_id, merchant_code, terminal_number +from redsys +; + +set role guest; +select bag_eq( + 'redsys_data', + $$ values (4, '444444444', 4) + , (8, '888888888', 8) + $$, + 'Everyone should be able to list Redsys settings from all companies' +); +reset role; + +select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog', 'co2'); + +select lives_ok( + $$ insert into redsys(company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (2, '222222222', 2, 'live', 'insite', E'\\x22' ) $$, + 'Admin from company 2 should be able to set up their Redsys settings.' +); + +select bag_eq( + 'redsys_data', + $$ values (2, '222222222', 2) + , (4, '444444444', 4) + , (8, '888888888', 8) + $$, + 'The new row should have been added' +); + +select lives_ok( + $$ update redsys set merchant_code = '121212121', terminal_number = 1, encrypt_key = E'\\x12' where company_id = 2 $$, + 'Admin from company 2 should be able to update their Redsys settings.' +); + +select bag_eq( + 'redsys_data', + $$ values (2, '121212121', 1) + , (4, '444444444', 4) + , (8, '888888888', 8) + $$, + 'The row should have been updated.' +); + +select lives_ok( + $$ delete from redsys where company_id = 2 $$, + 'Admin from company 2 should be able to delete their Redsys settings.' +); + +select bag_eq( + 'redsys_data', + $$ values (4, '444444444', 4) + , (8, '888888888', 8) + $$, + 'The row should have been deleted.' +); + +select throws_ok( + $$ insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (6, '666666666', 6, 'live', 'insite', E'\x66' ) $$, + '42501', 'new row violates row-level security policy for table "redsys"', + 'Admin from company 2 should NOT be able to setup Redsys for another company.' +); + +select lives_ok( + $$ update redsys set merchant_code = '123456789', terminal_number = 10 where company_id = 4 $$, + 'Admin from company 2 should NOT be able to update the Redsys settings of company 4, but no error' +); + +select bag_eq( + 'redsys_data', + $$ values (4, '444444444', 4) + , (8, '888888888', 8) + $$, + 'No row should have been updated.' +); + +select lives_ok( + $$ update redsys set company_id = 2 where company_id = 4 $$, + 'Admin from company 2 should NOT be able to usurp the Redsys settings of company 4, but no error because company_id is primary key' +); + +select lives_ok( + $$ delete from redsys where company_id = 4 $$, + 'Admin from company 2 should NOT be able to delete the Redsys of company 4, but not error is thrown' +); + +select bag_eq( + 'redsys_data', + $$ values (4, '444444444', 4) + , (8, '888888888', 8) + $$, + 'No row should have been deleted' +); + +select throws_ok( + $$ insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (2, '22222222', 1, 'live', 'insite', E'\\x22') $$, + '23514', 'new row for relation "redsys" violates check constraint "merchant_code_valid"', + 'Should not be able to insert a merchant code less than 9 digits long.' +); + +select throws_ok( + $$ insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (2, '2222222222', 1, 'live', 'insite', E'\\x22') $$, + '23514', 'new row for relation "redsys" violates check constraint "merchant_code_valid"', + 'Should not be able to insert a merchant code more than 9 digits long.' +); + +select throws_ok( + $$ insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (2, '2222a2222', 1, 'live', 'insite', E'\\x22') $$, + '23514', 'new row for relation "redsys" violates check constraint "merchant_code_valid"', + 'Should not be able to insert a merchant code that contains a non-digit character.' +); + +select throws_ok( + $$ insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (2, '222222222', 0, 'live', 'insite', E'\\x22') $$, + '23514', 'new row for relation "redsys" violates check constraint "terminal_number_in_range"', + 'Should not be able to insert a terminal code smaller than 1.' +); + +select throws_ok( + $$ insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) values (2, '222222222', 1000, 'live', 'insite', E'\\x22') $$, + '23514', 'new row for relation "redsys" violates check constraint "terminal_number_in_range"', + 'Should not be able to insert a terminal code greater than 999.' +); + +reset role; + + +select * +from finish(); + +rollback; + diff --git a/test/redsys_encrypt.sql b/test/redsys_encrypt.sql new file mode 100644 index 0000000..a2f3b87 --- /dev/null +++ b/test/redsys_encrypt.sql @@ -0,0 +1,49 @@ +-- Test redsys_encrypt +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(15); + +set search_path to camper, public; + +select has_function('camper', 'redsys_encrypt', array['integer', 'bytea']); +select function_lang_is('camper', 'redsys_encrypt', array['integer', 'bytea'], 'sql'); +select function_returns('camper', 'redsys_encrypt', array['integer', 'bytea'], 'bytea'); +select is_definer('camper', 'redsys_encrypt', array['integer', 'bytea']); +select volatility_is('camper', 'redsys_encrypt', array['integer', 'bytea'], 'stable'); +select function_privs_are('camper', 'redsys_encrypt', array ['integer', 'bytea'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'redsys_encrypt', array ['integer', 'bytea'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'redsys_encrypt', array ['integer', 'bytea'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'redsys_encrypt', array ['integer', 'bytea'], 'authenticator', array[]::text[]); + +set client_min_messages to warning; +truncate redsys cascade; +truncate company cascade; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) +values (2, '222222222', 1, 'live', 'insite', decode('x8RS+UkDkAN3i5hGU92U2c5BhJlffg8w', 'base64')) + , (4, '999008881', 1, 'test', 'insite', decode('sq7HjrUOBfKmC576ILgskD5srU870gJ7', 'base64')) +; + + +select is(redsys_encrypt(2, '\x61626364656667'::bytea), '\x5df27f7580163beb'::bytea); +select is(redsys_encrypt(2, '\x3030303030303031'::bytea), '\x6cf019b21fc92a68'::bytea); +select is(redsys_encrypt(2, '\x414243444546474849'::bytea), '\x3aa8d51dd4dfa79be2fa13de2c636f06'::bytea); + +select is(redsys_encrypt(4, '\x61626364656667'::bytea), '\x9b9cc420acfcd328'::bytea); +select is(redsys_encrypt(4, '\x3030303030303031'::bytea), '\x8ae3e9826d15ef02'::bytea); +select is(redsys_encrypt(4, '\x414243444546474849'::bytea), '\xce914bbfb35a70c38b7ea38cee8e7aea'::bytea); + + +select * +from finish(); + +rollback; diff --git a/test/redsys_environment.sql b/test/redsys_environment.sql new file mode 100644 index 0000000..89f3644 --- /dev/null +++ b/test/redsys_environment.sql @@ -0,0 +1,18 @@ +-- Test redsys_environment +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(2); + +set search_path to camper, public; + +select has_enum('camper', 'redsys_environment', 'Enum redsys_environment should exist'); +select enum_has_labels('camper', 'redsys_environment', array['test', 'live']); + +select * +from finish(); + +rollback; diff --git a/test/redsys_integration.sql b/test/redsys_integration.sql new file mode 100644 index 0000000..6ec2a63 --- /dev/null +++ b/test/redsys_integration.sql @@ -0,0 +1,18 @@ +-- Test redsys_integration +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(2); + +set search_path to camper, public; + +select has_enum('camper', 'redsys_integration', 'Enum type camper.redsys_integration should exist'); +select enum_has_labels('camper', 'redsys_integration', array['insite', 'redirect']); + +select * +from finish(); + +rollback; diff --git a/test/redsys_request.sql b/test/redsys_request.sql new file mode 100644 index 0000000..4fc1c58 --- /dev/null +++ b/test/redsys_request.sql @@ -0,0 +1,28 @@ +-- Test redsys_request +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(11); + +set search_path to camper, public; + +select has_composite('camper', 'redsys_request', 'Composite type camper.redsys_request should exist'); +select columns_are('camper', 'redsys_request', array['transaction_type', 'amount', 'order_number', 'product', 'card_holder', 'success_uri', 'failure_uri', 'notification_uri', 'lang_tag']); +select col_type_is('camper'::name, 'redsys_request'::name, 'transaction_type'::name, 'integer'); +select col_type_is('camper'::name, 'redsys_request'::name, 'amount'::name, 'text'); +select col_type_is('camper'::name, 'redsys_request'::name, 'order_number'::name, 'text'); +select col_type_is('camper'::name, 'redsys_request'::name, 'product'::name, 'text'); +select col_type_is('camper'::name, 'redsys_request'::name, 'card_holder'::name, 'text'); +select col_type_is('camper'::name, 'redsys_request'::name, 'success_uri'::name, 'uri'); +select col_type_is('camper'::name, 'redsys_request'::name, 'failure_uri'::name, 'uri'); +select col_type_is('camper'::name, 'redsys_request'::name, 'notification_uri'::name, 'uri'); +select col_type_is('camper'::name, 'redsys_request'::name, 'lang_tag'::name, 'text'); + + +select * +from finish(); + +rollback; diff --git a/test/redsys_sign_request.sql b/test/redsys_sign_request.sql new file mode 100644 index 0000000..269f8c0 --- /dev/null +++ b/test/redsys_sign_request.sql @@ -0,0 +1,49 @@ +-- Test redsys_sign_request +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(11); + +set search_path to camper, public; + +select has_function('camper', 'redsys_sign_request', array['integer', 'redsys_request']); +select function_lang_is('camper', 'redsys_sign_request', array['integer', 'redsys_request'], 'sql'); +select function_returns('camper', 'redsys_sign_request', array['integer', 'redsys_request'], 'redsys_signed_request'); +select isnt_definer('camper', 'redsys_sign_request', array['integer', 'redsys_request']); +select volatility_is('camper', 'redsys_sign_request', array['integer', 'redsys_request'], 'stable'); +select function_privs_are('camper', 'redsys_sign_request', array ['integer', 'redsys_request'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'redsys_sign_request', array ['integer', 'redsys_request'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'redsys_sign_request', array ['integer', 'redsys_request'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'redsys_sign_request', array ['integer', 'redsys_request'], 'authenticator', array[]::text[]); + +set client_min_messages to warning; +truncate redsys cascade; +truncate company cascade; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, country_code, currency_code, default_lang_tag) +values (2, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 'ES', 'EUR', 'ca') + , (4, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +insert into redsys (company_id, merchant_code, terminal_number, environment, integration, encrypt_key) +values (2, '222222222', 2, 'live', 'insite', decode('x8RS+UkDkAN3i5hGU92U2c5BhJlffg8w', 'base64')) + , (4, '999008881', 4, 'test', 'insite', decode('sq7HjrUOBfKmC576ILgskD5srU870gJ7', 'base64')) +; + +select is( + redsys_sign_request(2, row(0, '12.34', '00000001', 'Product #2', 'My Name', 'https://example.com/ok', 'https://example.com/ko', 'https://example.com/notification', 'ca')), + row('eyJEc19NZXJjaGFudF9NZXJjaGFudENvZGUiIDogIjIyMjIyMjIyMiIsICJEc19NZXJjaGFudF9UZXJtaW5hbCIgOiAiMiIsICJEc19NZXJjaGFudF9NZXJjaGFudE5hbWUiIDogIkNvbXBhbnkgMiIsICJEc19NZXJjaGFudF9UcmFuc2FjdGlvblR5cGUiIDogIjAiLCAiRHNfTWVyY2hhbnRfQW1vdW50IiA6ICIxMjM0IiwgIkRzX01lcmNoYW50X0N1cnJlbmN5IiA6ICI5NzgiLCAiRHNfTWVyY2hhbnRfT3JkZXIiIDogIjAwMDAwMDAxIiwgIkRzX01lcmNoYW50X1Byb2R1Y3RkZXNjcmlwdGlvbiIgOiAiUHJvZHVjdCAjMiIsICJEc19NZXJjaGFudF9UaXR1bGFyIiA6ICJNeSBOYW1lIiwgIkRzX01lcmNoYW50X1VybE9LIiA6ICJodHRwczovL2V4YW1wbGUuY29tL29rIiwgIkRzX01lcmNoYW50X1VybEtPIiA6ICJodHRwczovL2V4YW1wbGUuY29tL2tvIiwgIkRzX01lcmNoYW50X01lcmNoYW50VVJMIiA6ICJodHRwczovL2V4YW1wbGUuY29tL25vdGlmaWNhdGlvbiIsICJEc19NZXJjaGFudF9Db25zdW1lckxhbmd1YWdlIiA6ICIzIn0=', 'aELGpY6jRl48YXNFkRKuIhhddroJpdeFigsRPDsB3pQ=', 'HMAC_SHA256_V1')::redsys_signed_request +); + +select is( + redsys_sign_request(4, row(1, '2144.00', '0000ABCDE', 'Booking ABCDE', 'Customer Full Name', 'http://example.cat/success', 'http://example.cat/failure', 'http://example.cat/notify', 'en')), + row('eyJEc19NZXJjaGFudF9NZXJjaGFudENvZGUiIDogIjk5OTAwODg4MSIsICJEc19NZXJjaGFudF9UZXJtaW5hbCIgOiAiNCIsICJEc19NZXJjaGFudF9NZXJjaGFudE5hbWUiIDogIkNvbXBhbnkgNCIsICJEc19NZXJjaGFudF9UcmFuc2FjdGlvblR5cGUiIDogIjEiLCAiRHNfTWVyY2hhbnRfQW1vdW50IiA6ICIyMTQ0MDAiLCAiRHNfTWVyY2hhbnRfQ3VycmVuY3kiIDogIjg0MCIsICJEc19NZXJjaGFudF9PcmRlciIgOiAiMDAwMEFCQ0RFIiwgIkRzX01lcmNoYW50X1Byb2R1Y3RkZXNjcmlwdGlvbiIgOiAiQm9va2luZyBBQkNERSIsICJEc19NZXJjaGFudF9UaXR1bGFyIiA6ICJDdXN0b21lciBGdWxsIE5hbWUiLCAiRHNfTWVyY2hhbnRfVXJsT0siIDogImh0dHA6Ly9leGFtcGxlLmNhdC9zdWNjZXNzIiwgIkRzX01lcmNoYW50X1VybEtPIiA6ICJodHRwOi8vZXhhbXBsZS5jYXQvZmFpbHVyZSIsICJEc19NZXJjaGFudF9NZXJjaGFudFVSTCIgOiAiaHR0cDovL2V4YW1wbGUuY2F0L25vdGlmeSIsICJEc19NZXJjaGFudF9Db25zdW1lckxhbmd1YWdlIiA6ICIyIn0=', '5FybTC6lOiTUTeFCGiCZ7LCKb60Jt2CgCBz6QbfB/o0=', 'HMAC_SHA256_V1')::redsys_signed_request +); + +select * +from finish(); + +rollback; diff --git a/test/redsys_signed_request.sql b/test/redsys_signed_request.sql new file mode 100644 index 0000000..b2254ab --- /dev/null +++ b/test/redsys_signed_request.sql @@ -0,0 +1,22 @@ +-- Test redsys_signed_request +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(5); + +set search_path to camper, public; + +select has_composite('camper', 'redsys_signed_request', 'Composite type camper.redsys_signed_request should exist'); +select columns_are('camper', 'redsys_signed_request', array['merchant_parameters', 'signature', 'signature_version']); +select col_type_is('camper'::name, 'redsys_signed_request'::name, 'merchant_parameters'::name, 'text'); +select col_type_is('camper'::name, 'redsys_signed_request'::name, 'signature'::name, 'text'); +select col_type_is('camper'::name, 'redsys_signed_request'::name, 'signature_version'::name, 'text'); + + +select * +from finish(); + +rollback; diff --git a/test/setup_redsys.sql b/test/setup_redsys.sql new file mode 100644 index 0000000..996b36c --- /dev/null +++ b/test/setup_redsys.sql @@ -0,0 +1,78 @@ +-- Test setup_redsys +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(15); + +set search_path to camper, public; + +select has_function('camper', 'setup_redsys', array['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text']); +select function_lang_is('camper', 'setup_redsys', array['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'plpgsql'); +select function_returns('camper', 'setup_redsys', array['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'void'); +select isnt_definer('camper', 'setup_redsys', array['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text']); +select volatility_is('camper', 'setup_redsys', array['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'volatile'); +select function_privs_are('camper', 'setup_redsys', array ['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'guest', array[]::text[]); +select function_privs_are('camper', 'setup_redsys', array ['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'employee', array[]::text[]); +select function_privs_are('camper', 'setup_redsys', array ['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'setup_redsys', array ['integer', 'text', 'integer', 'redsys_environment', 'redsys_integration', 'text'], 'authenticator', array[]::text[]); + + +set client_min_messages to warning; +truncate redsys cascade; +truncate company cascade; + +insert into company (company_id, business_name, vatin, trade_name, phone, email, web, address, city, province, postal_code, rtc_number, country_code, currency_code, default_lang_tag) +values (1, 'Company 2', 'XX123', '', '555-555-555', 'a@a', '', '', '', '', '', '', 'ES', 'EUR', 'ca') + , (2, 'Company 4', 'XX234', '', '666-666-666', 'b@b', '', '', '', '', '', '', 'FR', 'USD', 'ca') +; + +prepare redsys_data as +select company_id, merchant_code, terminal_number, environment::text, integration::text, encrypt_key +from redsys +; + +select lives_ok( + $$ select setup_redsys(1, '111111111', 1, 'live', 'insite', encode(E'\\x1111', 'base64')) $$, + 'Should be able to setup Redsys parameters for the first company' +); + +select lives_ok( + $$ select setup_redsys(2, '222222222', 2, 'test', 'redirect', encode(E'\\x1212', 'base64')) $$, + 'Should be able to setup Redsys parameters for the second company' +); + +select bag_eq( + 'redsys_data', + $$ values (1, '111111111', 1, 'live', 'insite', E'\\x1111'::bytea), + (2, '222222222', 2, 'test', 'redirect', E'\\x1212'::bytea) + $$, + 'Should have inserted all Redsys parameters' +); + +select lives_ok( + $$ select setup_redsys(1, '999999999', 9, 'live', 'redirect', encode(E'\\x9119', 'base64')) $$, + 'Should be able to update Redsys parameters for the first company' +); + +select lives_ok( + $$ select setup_redsys(2, '123456789', 5, 'live', 'redirect', NULL) $$, + 'Should be able to update Redsys parameters for the second company, leaving the encrypt key alone' +); + +select bag_eq( + 'redsys_data', + $$ values (1, '999999999', 9, 'live', 'redirect', E'\\x9119'::bytea), + (2, '123456789', 5, 'live', 'redirect', E'\\x1212'::bytea) + $$, + 'Should have updated all Redsys parameters' +); + + +reset client_min_messages; +select * +from finish(); + +rollback; diff --git a/test/zero_pad.sql b/test/zero_pad.sql new file mode 100644 index 0000000..1dd853b --- /dev/null +++ b/test/zero_pad.sql @@ -0,0 +1,36 @@ +-- Test zero_pad +set client_min_messages to warning; +create extension if not exists pgtap; +reset client_min_messages; + +begin; + +select plan(19); + +set search_path to camper, public; + +select has_function('camper', 'zero_pad', array['bytea', 'integer']); +select function_lang_is('camper', 'zero_pad', array['bytea', 'integer'], 'sql'); +select function_returns('camper', 'zero_pad', array['bytea', 'integer'], 'bytea'); +select isnt_definer('camper', 'zero_pad', array['bytea', 'integer']); +select volatility_is('camper', 'zero_pad', array['bytea', 'integer'], 'immutable'); +select function_privs_are('camper', 'zero_pad', array ['bytea', 'integer'], 'guest', array['EXECUTE']); +select function_privs_are('camper', 'zero_pad', array ['bytea', 'integer'], 'employee', array['EXECUTE']); +select function_privs_are('camper', 'zero_pad', array ['bytea', 'integer'], 'admin', array['EXECUTE']); +select function_privs_are('camper', 'zero_pad', array ['bytea', 'integer'], 'authenticator', array[]::text[]); + +select is(zero_pad(''::bytea, 8), '\x'::bytea); +select is(zero_pad('\x11'::bytea, 8), '\x1100000000000000'::bytea); +select is(zero_pad('\x2211'::bytea, 8), '\x2211000000000000'::bytea); +select is(zero_pad('\x332211'::bytea, 8), '\x3322110000000000'::bytea); +select is(zero_pad('\x44332211'::bytea, 8), '\x4433221100000000'::bytea); +select is(zero_pad('\x5544332211'::bytea, 8), '\x5544332211000000'::bytea); +select is(zero_pad('\x665544332211'::bytea, 8), '\x6655443322110000'::bytea); +select is(zero_pad('\x77665544332211'::bytea, 8), '\x7766554433221100'::bytea); +select is(zero_pad('\x8877665544332211'::bytea, 8), '\x8877665544332211'::bytea); +select is(zero_pad('\x998877665544332211'::bytea, 8), '\x99887766554433221100000000000000'::bytea); + +select * +from finish(); + +rollback; diff --git a/verify/available_currencies.sql b/verify/available_currencies.sql index f308500..252e846 100644 --- a/verify/available_currencies.sql +++ b/verify/available_currencies.sql @@ -9,6 +9,7 @@ from currency where currency_code = 'EUR' and currency_symbol = '€' and decimal_digits = 2 + and redsys_code = 978 ; select 1 / count(*) @@ -16,6 +17,7 @@ from currency where currency_code = 'USD' and currency_symbol = '$' and decimal_digits = 2 + and redsys_code = 840 ; rollback; diff --git a/verify/available_languages.sql b/verify/available_languages.sql index 764a539..c38450e 100644 --- a/verify/available_languages.sql +++ b/verify/available_languages.sql @@ -10,7 +10,8 @@ where lang_tag = 'und' and name = 'Undefined' and endonym = 'Undefined' and not selectable - and currency_pattern = '%[3]s%.[1]*[2]f'; + and currency_pattern = '%[3]s%.[1]*[2]f' + and redsys_code = 0; select 1 / count(*) from language @@ -18,7 +19,8 @@ where lang_tag = 'ca' and name = 'Catalan' and endonym = 'català' and selectable - and currency_pattern = '%.[1]*[2]f %[3]s'; + and currency_pattern = '%.[1]*[2]f %[3]s' + and redsys_code = 3; select 1 / count(*) from language @@ -26,7 +28,8 @@ where lang_tag = 'en' and name = 'English' and endonym = 'English' and selectable - and currency_pattern = '%[3]s%.[1]*[2]f'; + and currency_pattern = '%[3]s%.[1]*[2]f' + and redsys_code = 2; select 1 / count(*) from language @@ -34,6 +37,7 @@ where lang_tag = 'es' and name = 'Spanish' and endonym = 'español' and selectable - and currency_pattern = '%.[1]*[2]f %[3]s'; + and currency_pattern = '%.[1]*[2]f %[3]s' + and redsys_code = 1; rollback; diff --git a/verify/currency.sql b/verify/currency.sql index 6b89c9a..6f72fc5 100644 --- a/verify/currency.sql +++ b/verify/currency.sql @@ -5,6 +5,7 @@ begin; select currency_code , currency_symbol , decimal_digits + , redsys_code from camper.currency where false; diff --git a/verify/encode_base64url.sql b/verify/encode_base64url.sql new file mode 100644 index 0000000..b642745 --- /dev/null +++ b/verify/encode_base64url.sql @@ -0,0 +1,7 @@ +-- Verify camper:encode_base64url on pg + +begin; + +select has_function_privilege('camper.encode_base64url(bytea)', 'execute'); + +rollback; diff --git a/verify/language.sql b/verify/language.sql index 2a74912..025f18c 100644 --- a/verify/language.sql +++ b/verify/language.sql @@ -7,6 +7,7 @@ select lang_tag , endonym , selectable , currency_pattern + , redsys_code from public.language where false; diff --git a/verify/redsys.sql b/verify/redsys.sql new file mode 100644 index 0000000..fc1b272 --- /dev/null +++ b/verify/redsys.sql @@ -0,0 +1,20 @@ +-- Verify camper:redsys on pg + +begin; + +select company_id + , merchant_code + , terminal_number + , environment + , integration + , encrypt_key +from camper.redsys +where false; + +select 1 / count(*) from pg_class where oid = 'camper.redsys'::regclass and relrowsecurity; +select 1 / count(*) from pg_policy where polname = 'guest_ok' and polrelid = 'camper.redsys'::regclass; +select 1 / count(*) from pg_policy where polname = 'insert_to_company' and polrelid = 'camper.redsys'::regclass; +select 1 / count(*) from pg_policy where polname = 'update_company' and polrelid = 'camper.redsys'::regclass; +select 1 / count(*) from pg_policy where polname = 'delete_from_company' and polrelid = 'camper.redsys'::regclass; + +rollback; diff --git a/verify/redsys_encrypt.sql b/verify/redsys_encrypt.sql new file mode 100644 index 0000000..8ee31a1 --- /dev/null +++ b/verify/redsys_encrypt.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_encrypt on pg + +begin; + +select has_function_privilege('camper.redsys_encrypt(integer, bytea)', 'execute'); + +rollback; diff --git a/verify/redsys_environment.sql b/verify/redsys_environment.sql new file mode 100644 index 0000000..81d7aa5 --- /dev/null +++ b/verify/redsys_environment.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_environment on pg + +begin; + +select has_type_privilege('camper.redsys_environment', 'usage'); + +rollback; diff --git a/verify/redsys_integration.sql b/verify/redsys_integration.sql new file mode 100644 index 0000000..b7d0aca --- /dev/null +++ b/verify/redsys_integration.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_integration on pg + +begin; + +select pg_catalog.has_type_privilege('camper.redsys_integration', 'usage'); + +rollback; diff --git a/verify/redsys_request.sql b/verify/redsys_request.sql new file mode 100644 index 0000000..e2f0245 --- /dev/null +++ b/verify/redsys_request.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_request on pg + +begin; + +select pg_catalog.has_type_privilege('camper.redsys_request', 'usage'); + +rollback; diff --git a/verify/redsys_sign_request.sql b/verify/redsys_sign_request.sql new file mode 100644 index 0000000..7eb4dfd --- /dev/null +++ b/verify/redsys_sign_request.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_sign_request on pg + +begin; + +select has_function_privilege('camper.redsys_sign_request(integer, camper.redsys_request)', 'execute'); + +rollback; diff --git a/verify/redsys_signed_request.sql b/verify/redsys_signed_request.sql new file mode 100644 index 0000000..86e8802 --- /dev/null +++ b/verify/redsys_signed_request.sql @@ -0,0 +1,7 @@ +-- Verify camper:redsys_signed_request on pg + +begin; + +select pg_catalog.has_type_privilege('camper.redsys_signed_request', 'usage'); + +rollback; diff --git a/verify/setup_redsys.sql b/verify/setup_redsys.sql new file mode 100644 index 0000000..5b18830 --- /dev/null +++ b/verify/setup_redsys.sql @@ -0,0 +1,7 @@ +-- Verify camper:setup_redsys on pg + +begin; + +select has_function_privilege('camper.setup_redsys(integer, text, integer, camper.redsys_environment, camper.redsys_integration, text)', 'execute'); + +rollback; diff --git a/verify/zero_pad.sql b/verify/zero_pad.sql new file mode 100644 index 0000000..2dcd2d9 --- /dev/null +++ b/verify/zero_pad.sql @@ -0,0 +1,7 @@ +-- Verify camper:zero_pad on pg + +begin; + +select has_function_privilege('camper.zero_pad(bytea, integer)', 'execute'); + +rollback;