diff --git a/backend/globaleaks/db/migration.py b/backend/globaleaks/db/migration.py index d3367101ca..dc67592f53 100644 --- a/backend/globaleaks/db/migration.py +++ b/backend/globaleaks/db/migration.py @@ -12,7 +12,7 @@ from globaleaks import __version__, models, \ DATABASE_VERSION, FIRST_DATABASE_VERSION_SUPPORTED, LANGUAGES_SUPPORTED_CODES from globaleaks.db.appdata import load_appdata, db_load_defaults -from globaleaks.db.migrations.update_69 import User_v_68, Tenant_v_68, Subscriber_v_68, InternalFile_v_68, \ +from globaleaks.db.migrations.update_69 import Field_v_68, InternalTipAnswers_v_68, User_v_68, Tenant_v_68, Subscriber_v_68, InternalFile_v_68, \ ReceiverFile_v_68 from globaleaks.orm import db_log @@ -57,7 +57,7 @@ ('Context', [Context_v_61, 0, 0, 0, 0, 0, 0, 0, 0, 0, Context_v_63, 0, models._Context, 0, 0, 0, 0, 0]), ('CustomTexts', [models._CustomTexts, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), ('EnabledLanguage', [models._EnabledLanguage, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), - ('Field', [models._Field, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + ('Field', [Field_v_68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, models._Field]), ('FieldAttr', [FieldAttr_v_52, models._FieldAttr, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), ('FieldOption', [models._FieldOption, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), ('FieldOptionTriggerField', [models._FieldOptionTriggerField, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), @@ -67,7 +67,7 @@ ('IdentityAccessRequestCustodian', [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, models._IdentityAccessRequestCustodian, 0, 0, 0, 0]), ('InternalFile', [InternalFile_v_64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, InternalFile_v_68, 0, 0, 0, models._InternalFile]), ('InternalTip', [InternalTip_v_52, InternalTip_v_57, 0, 0, 0, 0, InternalTip_v_59, 0, InternalTip_v_63, 0, 0, 0, InternalTip_v_64, InternalTip_v_66, 0, models._InternalTip, 0, 0]), - ('InternalTipAnswers', [models._InternalTipAnswers, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + ('InternalTipAnswers', [InternalTipAnswers_v_68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, models._InternalTipAnswers]), ('InternalTipData', [models._InternalTipData, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), ('Mail', [models._Mail, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), ('Message', [Message_v_64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1, -1, -1]), @@ -82,7 +82,7 @@ ('SubmissionStatusChange', [SubmissionStatusChange_v_54, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1]), ('Step', [models._Step, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), ('Subscriber', [Subscriber_v_52, Subscriber_v_62, 0, 0, 0, 0, 0, 0, 0, 0, 0, Subscriber_v_67, 0, 0, 0, 0, Subscriber_v_68, models._Subscriber]), - ('Tenant', [Tenant_v_52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, Tenant_v_68, models._Tenant]), + ('Tenant', [Tenant_v_52, Tenant_v_68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, models._Tenant]), ('User', [User_v_52, User_v_54, 0, User_v_56, 0, User_v_61, 0, 0, 0, 0, User_v_64, 0, 0, User_v_66, 0, User_v_68, 0, models._User]), ('WhistleblowerFile', [WhistleblowerFile_v_57, 0, 0, 0, 0, 0, WhistleblowerFile_v_64, 0, 0, 0, 0, 0, 0, WhistleblowerFile_v_66, 0, models._WhistleblowerFile, 0, 0]), ('InternalTipForwarding', [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, models._InternalTipForwarding]), diff --git a/backend/globaleaks/db/migrations/update_69/__init__.py b/backend/globaleaks/db/migrations/update_69/__init__.py index 64d1412e16..6324d7490e 100644 --- a/backend/globaleaks/db/migrations/update_69/__init__.py +++ b/backend/globaleaks/db/migrations/update_69/__init__.py @@ -1,7 +1,11 @@ +from globaleaks import models +from globaleaks.models.enums import EnumFieldInstance +from globaleaks.utils.crypto import GCE, Base64Encoder from globaleaks.db.migrations.update import MigrationBase from globaleaks.models import Model, EnumSubscriberStatus, EnumStateFile, EnumVisibility, EnumUserRole, EnumUserStatus from globaleaks.models.properties import * -from globaleaks.utils.utility import datetime_now, datetime_null +from globaleaks.utils.utility import datetime_never, datetime_now, datetime_null +from globaleaks.utils.log import log class Subscriber_v_68(Model): @@ -34,6 +38,30 @@ class Tenant_v_68(Model): active = Column(Boolean, default=False, nullable=False) +class Field_v_68(Model): + __tablename__ = 'field' + + id = Column(UnicodeText(36), primary_key=True, default=uuid4) + tid = Column(Integer, default=1, nullable=False) + x = Column(Integer, default=0, nullable=False) + y = Column(Integer, default=0, nullable=False) + width = Column(Integer, default=0, nullable=False) + label = Column(JSON, default=dict, nullable=False) + description = Column(JSON, default=dict, nullable=False) + hint = Column(JSON, default=dict, nullable=False) + placeholder = Column(JSON, default=dict, nullable=False) + required = Column(Boolean, default=False, nullable=False) + multi_entry = Column(Boolean, default=False, nullable=False) + triggered_by_score = Column(Integer, default=0, nullable=False) + step_id = Column(UnicodeText(36), index=True) + fieldgroup_id = Column(UnicodeText(36), index=True) + type = Column(UnicodeText, default='inputbox', nullable=False) + instance = Column(Enum(EnumFieldInstance), + default='instance', nullable=False) + template_id = Column(UnicodeText(36), index=True) + template_override_id = Column(UnicodeText(36), index=True) + + class InternalFile_v_68(Model): """ This model keeps track of submission files @@ -51,6 +79,18 @@ class InternalFile_v_68(Model): reference_id = Column(UnicodeText(36), default='', nullable=False) +class InternalTipAnswers_v_68(Model): + """ + This is the internal representation of Tip Questionnaire Answers + """ + __tablename__ = 'internaltipanswers' + + internaltip_id = Column(UnicodeText(36), primary_key=True) + questionnaire_hash = Column(UnicodeText(64), primary_key=True) + creation_date = Column(DateTime, default=datetime_now, nullable=False) + answers = Column(JSON, default=dict, nullable=False) + + class ReceiverFile_v_68(Model): """ This models stores metadata of files uploaded by recipients intended to bes @@ -93,14 +133,17 @@ class User_v_68(Model): mail_address = Column(UnicodeText, default='', nullable=False) language = Column(UnicodeText(12), nullable=False) password_change_needed = Column(Boolean, default=True, nullable=False) - password_change_date = Column(DateTime, default=datetime_null, nullable=False) + password_change_date = Column( + DateTime, default=datetime_null, nullable=False) crypto_prv_key = Column(UnicodeText(84), default='', nullable=False) crypto_pub_key = Column(UnicodeText(56), default='', nullable=False) crypto_rec_key = Column(UnicodeText(80), default='', nullable=False) crypto_bkp_key = Column(UnicodeText(84), default='', nullable=False) crypto_escrow_prv_key = Column(UnicodeText(84), default='', nullable=False) - crypto_escrow_bkp1_key = Column(UnicodeText(84), default='', nullable=False) - crypto_escrow_bkp2_key = Column(UnicodeText(84), default='', nullable=False) + crypto_escrow_bkp1_key = Column( + UnicodeText(84), default='', nullable=False) + crypto_escrow_bkp2_key = Column( + UnicodeText(84), default='', nullable=False) change_email_address = Column(UnicodeText, default='', nullable=False) change_email_token = Column(UnicodeText, unique=True) change_email_date = Column(DateTime, default=datetime_null, nullable=False) @@ -108,8 +151,10 @@ class User_v_68(Model): forcefully_selected = Column(Boolean, default=False, nullable=False) can_delete_submission = Column(Boolean, default=False, nullable=False) can_postpone_expiration = Column(Boolean, default=True, nullable=False) - can_grant_access_to_reports = Column(Boolean, default=False, nullable=False) - can_transfer_access_to_reports = Column(Boolean, default=False, nullable=False) + can_grant_access_to_reports = Column( + Boolean, default=False, nullable=False) + can_transfer_access_to_reports = Column( + Boolean, default=False, nullable=False) can_redact_information = Column(Boolean, default=False, nullable=False) can_mask_information = Column(Boolean, default=True, nullable=False) can_reopen_reports = Column(Boolean, default=True, nullable=False) @@ -121,19 +166,46 @@ class User_v_68(Model): # BEGIN of PGP key fields pgp_key_fingerprint = Column(UnicodeText, default='', nullable=False) pgp_key_public = Column(UnicodeText, default='', nullable=False) - pgp_key_expiration = Column(DateTime, default=datetime_null, nullable=False) + pgp_key_expiration = Column( + DateTime, default=datetime_null, nullable=False) # END of PGP key fields - accepted_privacy_policy = Column(DateTime, default=datetime_null, nullable=False) + accepted_privacy_policy = Column( + DateTime, default=datetime_null, nullable=False) clicked_recovery_key = Column(Boolean, default=False, nullable=False) class MigrationScript(MigrationBase): - def epilogue(self): - new_configuration = self.model_to['Config']() - new_configuration.var_name = 'url_file_analysis' - new_configuration.value = 'http://localhost/api/v1/scan' - self.session_new.add(new_configuration) + def add_global_stat_prv_key_to_users(self, global_stat_prv_key): + users = self.session_new.query(self.model_from['User']) \ + .filter(self.model_from['User'].tid == 1)\ + .filter(self.model_from['User'].role.in_([EnumUserRole.admin.name, EnumUserRole.analyst.name])) + + for user in users: + crypto_stat_key = Base64Encoder.encode( + GCE.asymmetric_encrypt(user.crypto_pub_key, global_stat_prv_key)).decode() + self.session_new.query(models.User) \ + .filter(models.User.id == user.id)\ + .update({'crypto_global_stat_prv_key': crypto_stat_key}) + + def add_global_stat_keys(self): + global_stat_prv_key, global_stat_pub_key = GCE.generate_keypair() + global_stat_pub_key_config = self.model_to['Config']() + global_stat_pub_key_config.var_name = 'global_stat_pub_key' + global_stat_pub_key_config.value = global_stat_pub_key + self.session_new.add(global_stat_pub_key_config) + self.entries_count['Config'] += 1 + self.add_global_stat_prv_key_to_users(global_stat_prv_key) + + def add_file_analisys_url(self): + file_analisys_config = self.model_to['Config']() + file_analisys_config.var_name = 'url_file_analysis' + file_analisys_config.value = 'http://localhost/api/v1/scan' + self.session_new.add(file_analisys_config) + log.info("FILE ANALISYS %s" % file_analisys_config.value) + self.entries_count['Config'] += 1 - self.entries_count['Config'] += 1 \ No newline at end of file + def epilogue(self): + self.add_file_analisys_url() + self.add_global_stat_keys() diff --git a/backend/globaleaks/handlers/wizard.py b/backend/globaleaks/handlers/wizard.py index ec5e3252a0..4cf19c8862 100644 --- a/backend/globaleaks/handlers/wizard.py +++ b/backend/globaleaks/handlers/wizard.py @@ -13,6 +13,17 @@ from globaleaks.utils.log import log from globaleaks.utils.sock import isIPAddress +def generate_analyst_key_pair(session, admin_user): + global_stat_prv_key, global_stat_pub_key = GCE.generate_keypair() + global_stat_pub_key_config = session.query(models.Config) \ + .filter(models.Config.tid == 1, models.Config.var_name == 'global_stat_pub_key') + global_stat_pub_key_config.value = global_stat_pub_key + + crypto_stat_key = Base64Encoder.encode( + GCE.asymmetric_encrypt(admin_user.crypto_pub_key, global_stat_prv_key)).decode() + session.query(models.User) \ + .filter(models.User.id == admin_user.id)\ + .update({'crypto_global_stat_prv_key': crypto_stat_key}) def db_wizard(session, tid, hostname, request): """ @@ -73,6 +84,8 @@ def db_wizard(session, tid, hostname, request): admin_user = db_create_user(session, tid, None, admin_desc, language) db_set_user_password(session, tid, admin_user, request['admin_password']) admin_user.password_change_needed = (tid != 1) + if tid == 1: + generate_analyst_key_pair(session, admin_user) if encryption and escrow: node.set_val('crypto_escrow_pub_key', crypto_escrow_pub_key) diff --git a/backend/globaleaks/models/__init__.py b/backend/globaleaks/models/__init__.py index 779e3cb52d..3efd66e907 100644 --- a/backend/globaleaks/models/__init__.py +++ b/backend/globaleaks/models/__init__.py @@ -422,6 +422,7 @@ class _Field(Model): instance = Column(Enum(EnumFieldInstance), default='instance', nullable=False) template_id = Column(UnicodeText(36), index=True) template_override_id = Column(UnicodeText(36), index=True) + statistical = Column(Boolean, default=False, nullable=False) @declared_attr def __table_args__(self): @@ -1068,6 +1069,7 @@ class _User(Model): crypto_escrow_prv_key = Column(UnicodeText(84), default='', nullable=False) crypto_escrow_bkp1_key = Column(UnicodeText(84), default='', nullable=False) crypto_escrow_bkp2_key = Column(UnicodeText(84), default='', nullable=False) + crypto_global_stat_prv_key = Column(UnicodeText(84), default='', nullable=True) change_email_address = Column(UnicodeText, default='', nullable=False) change_email_token = Column(UnicodeText, unique=True) change_email_date = Column(DateTime, default=datetime_null, nullable=False) diff --git a/backend/globaleaks/models/config.py b/backend/globaleaks/models/config.py index 9d4e7f1768..54b705c696 100644 --- a/backend/globaleaks/models/config.py +++ b/backend/globaleaks/models/config.py @@ -162,6 +162,7 @@ def initialize_config(session, tid, mode): variables[name] = root_tenant_node[name] variables['url_file_analysis'] = 'http://localhost/api/v1/scan' + variables['global_stat_pub_key'] = '' for name, value in variables.items(): session.add(Config({'tid': tid, 'var_name': name, 'value': value})) diff --git a/backend/globaleaks/models/config_desc.py b/backend/globaleaks/models/config_desc.py index 3a4b4d267b..cc241c992d 100644 --- a/backend/globaleaks/models/config_desc.py +++ b/backend/globaleaks/models/config_desc.py @@ -119,7 +119,8 @@ class Bool(Item): 'version_db': Int(default=DATABASE_VERSION), 'wizard_done': Bool(default=False), 'uuid': Unicode(default=uuid4), - 'url_file_analysis': Unicode(default='http://localhost/api/v1/scan') + 'url_file_analysis': Unicode(default='http://localhost/api/v1/scan'), + 'global_stat_pub_key': Unicode(default='') }