-
Notifications
You must be signed in to change notification settings - Fork 362
Fix/eng 11829 2 #11885
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/gdpr-delete-orcid-rewrite
Are you sure you want to change the base?
Fix/eng 11829 2 #11885
Changes from all commits
68da146
4c48222
eebddd6
b364f1f
f5aa332
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| from django.db import migrations | ||
| import osf.utils.datetime_aware_jsonfield | ||
| import osf.utils.fields | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('osf', '0052_downloadevent_download_channel'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name='osfuser', | ||
| name='external_identity_tokens', | ||
| field=osf.utils.datetime_aware_jsonfield.DateTimeAwareJSONField(blank=True, default=dict, encoder=osf.utils.datetime_aware_jsonfield.DateTimeAwareJSONEncoder), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| # OSF imports | ||
| import itsdangerous | ||
| import pytz | ||
| import requests | ||
| from dirtyfields import DirtyFieldsMixin | ||
|
|
||
| from django.conf import settings | ||
|
|
@@ -319,6 +320,16 @@ class OSFUser(DirtyFieldsMixin, GuidMixin, BaseModel, AbstractBaseUser, Permissi | |
| # }, | ||
| # ... | ||
| # } | ||
| external_identity_tokens = DateTimeAwareJSONField(default=dict, blank=True) | ||
| # Format: { | ||
| # <external_id_provider>: { | ||
| # <external_id>: { | ||
| # "access_token" : <token>, | ||
| # } | ||
| # ... | ||
| # }, | ||
| # ... | ||
| # } | ||
|
|
||
| # Employment history | ||
| jobs = DateTimeAwareJSONField(default=list, blank=True, validators=[validate_history_item]) | ||
|
|
@@ -817,7 +828,12 @@ def merge_user(self, user): | |
| self.external_identity[service] = { | ||
| service_id: status | ||
| } | ||
|
|
||
| token_entry = user.external_identity_tokens.get(service, {}).get(service_id) | ||
| if token_entry: | ||
| self.external_identity_tokens.setdefault(service, {})[service_id] = token_entry | ||
| user.external_identity = {} | ||
| user.external_identity_tokens = {} | ||
|
|
||
| # FOREIGN FIELDS | ||
| self.external_accounts.add(*user.external_accounts.values_list('pk', flat=True)) | ||
|
|
@@ -2137,6 +2153,50 @@ def _clear_identifying_information(self): | |
| ''' | ||
| This method ensures a user's info is deleted during a GDPR delete | ||
| ''' | ||
| # A user has at most one ORCID identity, so there is at most one token entry to revoke. | ||
| orcid_id, token_entry = next(iter(self.external_identity_tokens.get('ORCID', {}).items()), (None, None)) | ||
| orcid_token = token_entry.get('access_token') if token_entry else None | ||
| sentry.log_message( | ||
| f'[GDPR delete; _clear_identifying_information] user={self._id}: ' | ||
| f'{"found" if orcid_token else "no"} ORCID token to revoke', | ||
| level=logging.INFO, | ||
| ) | ||
| if not (orcid_id and orcid_token): | ||
| raise UserStateError( | ||
| 'User do not have connected ORCID' | ||
| ) | ||
| else: | ||
| sentry.log_message( | ||
| f'[GDPR delete] user={self._id}: revoking ORCID id={orcid_id} ' | ||
| f'via {website_settings.ORCID_OAUTH_REVOKE_URL}', | ||
| level=logging.INFO, | ||
| ) | ||
| try: | ||
| response = requests.post( | ||
| website_settings.ORCID_OAUTH_REVOKE_URL, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is the revoke URL so I don't think we need the one in CAS
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add this one to website settings |
||
| data={ | ||
| 'client_id': website_settings.ORCID_OAUTH_CLIENT_ID, | ||
| 'client_secret': website_settings.ORCID_OAUTH_CLIENT_SECRET, | ||
| 'token': orcid_token, | ||
| }, | ||
| timeout=website_settings.ORCID_OAUTH_REVOKE_REQUEST_TIMEOUT, | ||
| ) | ||
| sentry.log_message( | ||
| f'[GDPR delete] user={self._id}: ORCID id={orcid_id} revoked, ' | ||
| f'status_code={response.status_code}, response_text={response.text}', | ||
| level=logging.INFO, | ||
| ) | ||
| response.raise_for_status() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add unit tests for success, invalid client_id/secret, invalid token, empty token |
||
| except requests.exceptions.RequestException as e: | ||
| sentry.log_message( | ||
| f'[GDPR delete] Failed to revoke ORCID token for user {self._id} ORCID id {orcid_id}: {e}', | ||
| level=logging.ERROR, | ||
| ) | ||
| sentry.log_exception(e) | ||
| raise UserStateError( | ||
| 'Fail to revoke ORCID\'s service could not be reached' | ||
| ) | ||
|
|
||
| # This doesn't remove identifying info, but ensures other users can't see the deleted user's profile etc. | ||
| self.deactivate_account() | ||
|
|
||
|
|
@@ -2172,7 +2232,9 @@ def _clear_identifying_information(self): | |
| account.profile_url = None | ||
| account.save() | ||
| self.external_accounts.clear() | ||
|
|
||
| self.external_identity = {} | ||
| self.external_identity_tokens = {} | ||
| self.deleted = timezone.now() | ||
|
|
||
| @property | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only log message to sentry if access token or refresh token are missing, level is warning