From 2473734790e8f85bd0e598950bf9b3a690bb5c58 Mon Sep 17 00:00:00 2001 From: Jonathan Druart Date: Fri, 22 Nov 2019 17:37:42 +0100 Subject: [PATCH] Bug 24151: Copy info to the pseudonymized table when a patron is modified This is the commit where you will find useful information about this development. The goal of this neww feature is to add a way to pseudonymize patron's data, in a way they could not be personally identifiable. https://en.wikipedia.org/wiki/Pseudonymization There are different existing way to anonymize patron's information in Koha, but we loose the ability to make useful report. This development proposes to have 3 different tables: * 1 for patrons' data (anonymized_borrowers) * 1 for transactions (anonymized_transactions) * 1 for patrons' attributes (anonymized_borrower_attributes) Entries to anonymized_borrowers are added when a new patron is added or modified, to anonymized_transactions when a new transaction (checkout, checkin, renew, on-site checkout) is done, and anonymized_borrower_attributes when attributes are added or modified. That way we are maintaining up-to-date informations in 3 different tables. To make those informations not identifiable to a patron, we are having a hashed_borrowernumber column in each of the 3 tables. This hash will be generated (Blowfish-based crypt) using a key stored in the Koha configuration. To make things configurable, we are adding 3 sysprefs and 1 new DB column: * syspref Pseudonymization to turn on/off the whole feature * syspref PseudonymizationPatronFields to list the informations of the patrons to sync * syspref PseudonymizationTransactionFields to list the informations of the transactions to copy * DB column borrower_attribute_types.keep_for_anonymized that is a boolean to enable/disable the copy of a given patron's attribute type. Test plan: 1/ Turn on Pseudonymization 2/ Define in PseudonymizationPatronFields and PseudonymizationTransactionFields the different fields you want to copy 3/ Go to the about page => You will see a warning about a missing config entry 4/ You need to generate a key and put it in the koha-conf.xml file. The following command will generate one: % htpasswd -bnBC 10 "" password | tr -d ':\n' | sed 's/$2y/$2a/' Then edit $KOHA_CONF and add it before of the end of the config section ($2a$10$PfdrEBdRcL2MZlEtKueyLegxI6zg735jD07GRnc1bt.N/ZYMvBAB2 5/ Restart memcached then plack (alias restart_all) => Everything is setup! 6/ Create a new patron => Confirm that a new entry has been added to anonymized_borrowers with the data you expect to be copied 7/ Modify an existing patron (ie. that does not have a corresponding entry in anonymized_borrowers) => Confirm that a new entry has been added to anonymized_borrowers with the data you expect to be copied 8/ Add some transactions (checkout, checkin, renew, on-site checkout) for patron that existed before you turned the pref Pseudonymization on and for newly created/modified patrons => Confirm that new entries have been added to anonymized_transactions with the data you expect 9/ Edit some patron attribute types and tick "Keep for pseudonymization" 10/ Edit or create patrons and add data for those patron attributes => Confirm that new entries have been added to anonymized_borrower_attributese 11/ Delete the patrons => Confirm that the entries still exist in the anonymized_* tables 12/ Purge the patrons (ie. use cleanup_database.pl to remove them from the deleted_borrowers table) => Confirm that the entries still exist in the anonymized_* tables See bug 24152 to remove data from the anonymized_* tables Sponsored-by: Association KohaLa - https://koha-fr.org/ --- Koha/Anonymized/Patron.pm | 82 ++++++++++++++++++++++++++++++++++++++++++++++ Koha/Anonymized/Patrons.pm | 64 ++++++++++++++++++++++++++++++++++++ Koha/Patron.pm | 14 +++++++- 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 Koha/Anonymized/Patron.pm create mode 100644 Koha/Anonymized/Patrons.pm diff --git a/Koha/Anonymized/Patron.pm b/Koha/Anonymized/Patron.pm new file mode 100644 index 0000000000..1cd5ba9c2c --- /dev/null +++ b/Koha/Anonymized/Patron.pm @@ -0,0 +1,82 @@ +package Koha::Anonymized::Patron; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Carp; +use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64); + +use Koha::Database; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::Anonymized::Patron - Koha Anonymized::Patron Object class + +=head1 API + +=head2 Class methods + +=head3 new + +=cut + +sub get_hash { + my ( $class, $borrowernumber) = @_; + return bcrypt($borrowernumber, C4::Context->config('key')); +} + + +sub new_from_patron { + my ( $class, $patron ) = @_; + + my $self = $class->SUPER::new({ + hashed_borrowernumber => $class->get_hash($patron->borrowernumber), + }); + + $self->update_from_patron($patron); + return $self; +} + +sub update_from_patron { + my ( $self, $patron ) = @_; + + my @fields_to_copy = split ',', C4::Context->preference('PseudonymizationPatronFields') || ''; + my $values = + { map { $_ => $patron->$_ } @fields_to_copy }; + + $values->{branchcode} = $patron->branchcode; # FIXME Must be removed from the pref options, or FK removed (?) + $values->{categorycode} = $patron->categorycode; + + $values->{has_cardnumber} = $patron->cardnumber ? 1 : 0; + + $self->set($values); + return $self; +} + +=head2 Internal methods + +=head3 _type + +=cut + +sub _type { + return 'AnonymizedBorrower'; +} + +1; diff --git a/Koha/Anonymized/Patrons.pm b/Koha/Anonymized/Patrons.pm new file mode 100644 index 0000000000..c3b57ec518 --- /dev/null +++ b/Koha/Anonymized/Patrons.pm @@ -0,0 +1,64 @@ +package Koha::Anonymized::Patrons; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Carp; +use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64); + +use Koha::Database; + +use Koha::Anonymized::Patron; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::Anonymized::Patrons - Koha Anonymized Patron Object set class + +=head1 API + +=cut + +sub find_from_borrowernumber { + my ($class, $borrowernumber) = @_; + my $hash = $class->get_hash($borrowernumber); + return $class->find($hash); +} +sub get_hash { + my ( $class, $borrowernumber) = @_; + return bcrypt($borrowernumber, C4::Context->config('key')); +} + + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub _type { + return 'AnonymizedBorrower'; +} + +sub object_class { + return 'Koha::Anonymized::Patron'; +} + +1; diff --git a/Koha/Patron.pm b/Koha/Patron.pm index 3f72ba6519..5894dedaac 100644 --- a/Koha/Patron.pm +++ b/Koha/Patron.pm @@ -28,6 +28,7 @@ use Text::Unaccent qw( unac_string ); use C4::Context; use C4::Log; use Koha::Account; +use Koha::Anonymized::Patrons; use Koha::AuthUtils; use Koha::Checkouts; use Koha::Club::Enrollments; @@ -186,6 +187,8 @@ sub store { $self->fixup_cardnumber; } + my $in_storage = $self->in_storage; + unless( $self->category->in_storage ) { Koha::Exceptions::Object::FKConstraint->throw( broken_fk => 'categorycode', @@ -199,7 +202,7 @@ sub store { $self->surname( uc($self->surname) ) if C4::Context->preference("uppercasesurnames"); - unless ( $self->in_storage ) { #AddMember + unless ( $in_storage ) { #AddMember # Generate a valid userid/login if needed $self->generate_userid @@ -338,6 +341,15 @@ sub store { # Final store $self = $self->SUPER::store; } + + if ( C4::Context->preference('Pseudonymization') ) { + unless ( $in_storage ) { + Koha::Anonymized::Patron->new_from_patron($self)->store; + } else { + Koha::Anonymized::Patrons->find_from_borrowernumber($self->borrowernumber)->update_from_patron($self)->store; + } + } + } ); return $self; -- 2.11.0