diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index 9c7a0633da8..69c183fad39 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -286,6 +286,7 @@ class Configuration(object): for ip in kwargs.get("fetch_url_whitelist", "").split(',') if len(ip.strip()) > 0 ] + self.gdpr_compliance_mode = string_as_bool(kwargs.get("gdpr_compliance_mode", "False")) self.allow_user_creation = string_as_bool(kwargs.get("allow_user_creation", "True")) self.allow_user_deletion = string_as_bool(kwargs.get("allow_user_deletion", "False")) self.allow_user_dataset_purge = string_as_bool(kwargs.get("allow_user_dataset_purge", "True")) diff --git a/lib/galaxy/tools/error_reports/plugins/biostars.py b/lib/galaxy/tools/error_reports/plugins/biostars.py index d063209c2b8..c23d11dfc74 100644 --- a/lib/galaxy/tools/error_reports/plugins/biostars.py +++ b/lib/galaxy/tools/error_reports/plugins/biostars.py @@ -18,12 +18,19 @@ class BiostarsPlugin(ErrorPlugin): def __init__(self, **kwargs): self.app = kwargs['app'] + self.gdpr_compliant = self.app.config.gdpr_compliance_mode self.verbose = string_as_bool(kwargs.get('verbose', True)) self.user_submission = string_as_bool(kwargs.get('user_submission', True)) def submit_report(self, dataset, job, tool, **kwargs): """Doesn't do anything, just shows a link to submit on biostars. """ + # This class specifically does nothing special for GDPR compliance + # because the user is willingly posting their data on a public + # first/third-party site. Maybe should do something about the dialog + # linking to the biostars privacy policy during the "submit to + # biostars" dialog? + try: assert biostar.biostar_enabled(self.app), ValueError("Biostar is not configured for this galaxy instance") assert self.app.config.biostar_enable_bug_reports, ValueError("Biostar is not configured to allow bug reporting for this galaxy instance") diff --git a/lib/galaxy/tools/error_reports/plugins/email.py b/lib/galaxy/tools/error_reports/plugins/email.py index 6d231e8281b..2333ecd1314 100644 --- a/lib/galaxy/tools/error_reports/plugins/email.py +++ b/lib/galaxy/tools/error_reports/plugins/email.py @@ -17,6 +17,7 @@ class EmailPlugin(ErrorPlugin): def __init__(self, **kwargs): self.app = kwargs['app'] + self.gdpr_compliant = self.app.config.gdpr_compliance_mode self.verbose = string_as_bool(kwargs.get('verbose', True)) self.user_submission = string_as_bool(kwargs.get('user_submission', True)) @@ -25,7 +26,7 @@ class EmailPlugin(ErrorPlugin): """ try: error_reporter = EmailErrorReporter(dataset.id, self.app) - error_reporter.send_report(user=job.get_user(), email=kwargs.get('email', None), message=kwargs.get('message', None)) + error_reporter.send_report(user=job.get_user(), email=kwargs.get('email', None), message=kwargs.get('message', None), gdpr_compliant=self.gdpr_compliant) return ("Your error report has been sent", "success") except Exception as e: return ("An error occurred sending the report by email: %s" % str(e), "danger") diff --git a/lib/galaxy/tools/error_reports/plugins/github.py b/lib/galaxy/tools/error_reports/plugins/github.py index baa756e1aa8..eaac22fcb88 100644 --- a/lib/galaxy/tools/error_reports/plugins/github.py +++ b/lib/galaxy/tools/error_reports/plugins/github.py @@ -17,6 +17,7 @@ class GithubPlugin(ErrorPlugin): def __init__(self, **kwargs): self.app = kwargs['app'] + self.gdpr_compliant = self.app.config.gdpr_compliance_mode self.verbose = string_as_bool(kwargs.get('verbose', False)) self.user_submission = string_as_bool(kwargs.get('user_submission', False)) try: @@ -69,7 +70,7 @@ class GithubPlugin(ErrorPlugin): # We'll re-use the email error reporter's template since github supports HTML error_reporter = EmailErrorReporter(dataset.id, self.app) - error_reporter.create_report(job.get_user(), email=kwargs.get('email', None), message=kwargs.get('message', None)) + error_reporter.create_report(job.get_user(), email=kwargs.get('email', None), message=kwargs.get('message', None), gdpr_compliant=self.gdpr_compliant) # The HTML report error_message = error_reporter.html_report diff --git a/lib/galaxy/tools/error_reports/plugins/json.py b/lib/galaxy/tools/error_reports/plugins/json.py index c1ce581216c..8db659b44ae 100644 --- a/lib/galaxy/tools/error_reports/plugins/json.py +++ b/lib/galaxy/tools/error_reports/plugins/json.py @@ -20,6 +20,7 @@ class JsonPlugin(ErrorPlugin): def __init__(self, **kwargs): self.app = kwargs['app'] self.verbose = string_as_bool(kwargs.get('verbose', False)) + self.gdpr_compliant = self.app.config.gdpr_compliance_mode self.user_submission = string_as_bool(kwargs.get('user_submission', False)) self.report_directory = kwargs.get("directory", tempfile.gettempdir()) if not os.path.exists(self.report_directory): @@ -40,12 +41,17 @@ class JsonPlugin(ErrorPlugin): 'exit_code': job.exit_code, 'stdout': job.stdout, 'handler': job.handler, - 'user': job.get_user().to_dict(), 'tool_version': job.tool_version, 'tool_xml': str(tool.config_file) if tool else None } - if 'email' in kwargs: - data['email'] = kwargs['email'] + if self.gdpr_compliant: + data['user'] = { + 'id': job.get_user().id + } + else: + data['user'] = job.get_user().to_dict() + if 'email' in kwargs: + data['email'] = kwargs['email'] if 'message' in kwargs: data['message'] = kwargs['message'] diff --git a/lib/galaxy/tools/error_reports/plugins/sentry.py b/lib/galaxy/tools/error_reports/plugins/sentry.py index be42ea80752..0a1f0aabcd0 100644 --- a/lib/galaxy/tools/error_reports/plugins/sentry.py +++ b/lib/galaxy/tools/error_reports/plugins/sentry.py @@ -29,6 +29,7 @@ class SentryPlugin(ErrorPlugin): def __init__(self, **kwargs): self.app = kwargs['app'] + self.gdpr_compliant = self.app.config.gdpr_compliance_mode self.verbose = string_as_bool(kwargs.get('verbose', False)) self.user_submission = string_as_bool(kwargs.get('user_submission', False)) self.custom_dsn = kwargs.get('custom_dsn', None) @@ -61,8 +62,11 @@ class SentryPlugin(ErrorPlugin): 'tool_version': unicodify(job.tool_version), 'tool_xml': unicodify(tool.config_file) if tool else None } - if 'email' in kwargs: - extra['email'] = unicodify(kwargs['email']) + if self.gdpr_compliant: + extra['email'] = 'gdpr-redacted' + else: + if 'email' in kwargs: + extra['email'] = unicodify(kwargs['email']) # User submitted message extra['message'] = unicodify(kwargs.get('message', '')) @@ -86,13 +90,20 @@ class SentryPlugin(ErrorPlugin): # The above does not work when handlers are separate from the web handlers url = None - if user: - # User information here also places email links + allows seeing - # a list of affected users in the tags/filtering. - context['user'] = { - 'name': user.username, - 'email': user.email, - } + if self.gdpr_compliant: + if user: + # Opauqe identifier + context['user'] = { + 'id': user.id + } + else: + if user: + # User information here also places email links + allows seeing + # a list of affected users in the tags/filtering. + context['user'] = { + 'name': user.username, + 'email': user.email, + } context['request'] = {'url': url} diff --git a/lib/galaxy/tools/errors.py b/lib/galaxy/tools/errors.py index eab04dc805c..ec78996a576 100644 --- a/lib/galaxy/tools/errors.py +++ b/lib/galaxy/tools/errors.py @@ -154,7 +154,7 @@ class ErrorReporter(object): roles = [] return self.app.security_agent.can_access_dataset(roles, self.hda.dataset) - def create_report(self, user, email='', message='', **kwd): + def create_report(self, user, email='', message='', gdpr_compliant=False, **kwd): hda = self.hda job = self.job host = web.url_for('/', qualified=True) @@ -163,10 +163,29 @@ class ErrorReporter(object): hda_id_encoded = self.app.security.encode_id(hda.id) hda_show_params_link = web.url_for(controller="dataset", action="show_params", dataset_id=hda_id_encoded, qualified=True) # Build the email message - if user and user.email != email: - email_str = "'%s' (providing preferred contact email '%s')" % (user.email, email) + if self.gdpr_compliant: + # This is sub-optimal but it is hard to solve fully. This affects + # the github posting method more than the traditional email plugin. + # There is no way around CCing the person with the traditional + # email bug report plugin, however with the GitHub plugin we can + # submit to GH without putting the email in the bug report. + # + # A secondary system with access to the github issue and access to + # the galaxy database can shuttle email back and forth between + # github comments and user-emails. Something like + # https://github.com/erasche/github-issue-to-mailgun-gateway + # Thus preventing issue helpers from every knowing the identity of + # the bug reporter (and preventing information about the bug + # reporter from leaving the EU until it hits email directly to the + # user.) + email_str = 'gdpr-redacted' + if user: + email_str += ' (user: %s)' % user.id else: - email_str = "'%s'" % (email or 'anonymously') + if user and user.email != email: + email_str = "'%s' (providing preferred contact email '%s')" % (user.email, email) + else: + email_str = "'%s'" % (email or 'anonymously') report_variables = dict( host=host, diff --git a/lib/galaxy/webapps/galaxy/config_schema.yml b/lib/galaxy/webapps/galaxy/config_schema.yml index 7aa53b0b6ab..4e20f00ad82 100644 --- a/lib/galaxy/webapps/galaxy/config_schema.yml +++ b/lib/galaxy/webapps/galaxy/config_schema.yml @@ -2039,7 +2039,10 @@ mapping: desc: | Enables GDPR Compliance mode. This makes several changes to the way Galaxy logs and exposes data externally such as removing - emails/usernames from logs and bug reports. + emails/usernames from logs and bug reports. It also causes the delete + user admin action to permanently delete all information stored on a + user, removing all related rows from the database. (You are + responsible for removing the data from backups.) enable_beta_ts_api_install: type: bool