Skip to content

Fix/eng 11829 2 - #11885

Open
Vlad0n20 wants to merge 5 commits into
CenterForOpenScience:feature/gdpr-delete-orcid-rewritefrom
Vlad0n20:fix/ENG-11829-2
Open

Fix/eng 11829 2#11885
Vlad0n20 wants to merge 5 commits into
CenterForOpenScience:feature/gdpr-delete-orcid-rewritefrom
Vlad0n20:fix/ENG-11829-2

Conversation

@Vlad0n20

@Vlad0n20 Vlad0n20 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Ticket

Purpose

Changes

Side Effects

QE Notes

CE Notes

Documentation

@cslzchen cslzchen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1st pass done

Comment thread framework/auth/cas.py
Comment on lines +276 to +280
sentry.log_message(
f'CAS response ORCID attributes: user=[{user._id}], orcidId=[{orcid_id}], '
f'orcidAccessToken=[{"present" if access_token else "missing"}]',
level=logging.INFO,
)

Copy link
Copy Markdown
Collaborator

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

Comment thread osf/models/user.py Outdated
# },
# ...
# }
external_identity_access_token = DateTimeAwareJSONField(default=dict, blank=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

external_identity_tokens

Comment thread framework/auth/cas.py Outdated
Comment on lines +315 to +316
access_token = cas_resp.attributes.get('orcidAccessToken', None)
refresh_token = cas_resp.attributes.get('orcidRefreshToken', None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move into if external_credential:

Comment thread framework/auth/cas.py Outdated
external_credential['id'],
access_token,
refresh_token,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this

Comment thread osf/models/user.py Outdated
f'found {len(orcid_tokens)} ORCID token(s) to revoke',
level=logging.INFO,
)
for orcid_id, orcid_token in orcid_tokens.items():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check if there is an external_identity but there is no matching external_identity_tokens, we need to fail the GDPR delete with relevant error messsage.

Comment thread osf/models/user.py Outdated
'client_secret': website_settings.ORCID_OAUTH_CLIENT_SECRET,
'token': orcid_token,
},
timeout=5,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future, we probably want a retry just in case

Comment thread osf/models/user.py Outdated
)
sentry.log_message(
f'[GDPR delete] user={self._id}: ORCID id={orcid_id} revoked, '
f'status_code={response.status_code}, response_text={response.text}, response={response}',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably don't need the full response

Comment thread osf/models/user.py
f'status_code={response.status_code}, response_text={response.text}, response={response}',
level=logging.INFO,
)
response.raise_for_status()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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

Comment thread osf/models/user.py Outdated
response.raise_for_status()
except requests.exceptions.RequestException as e:
sentry.log_message(
f'[GDPR delete] Failed to revoke ORCID token for user {self._id}: {e}',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the orcid id just in case user has mulitple

Comment thread osf/models/user.py Outdated
Comment on lines +2196 to +2197
'Unable to revoke this user\'s ORCID access right now because ORCID\'s '
'service could not be reached. Please try the GDPR delete again later.'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rephrase to a more general message like fail to revoke

@cslzchen cslzchen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2nd pass done ⭐

Comment thread framework/auth/cas.py Outdated
provider = settings.EXTERNAL_IDENTITY_PROFILE['OrcidProfile']
user.external_identity_access_token.setdefault(provider, {})[orcid_id] = {
'access_token': access_token,
'refresh_token': refresh_token,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove refresh_token due to CAS limit

Comment thread osf/models/user.py Outdated
Comment on lines +2196 to +2199
else:
raise UserStateError(
'User do not have connected ORCID'
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to if statement first,

if failure
    raise/return
the rest for success

Comment thread osf/models/user.py Outdated
self.external_accounts.clear()

self.external_identity = {}
self.external_identity_access_token = {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename

SPAM_SUBMIT_TASK_HARD_TIME_LIMIT = 90

CAS_SERVER_URL = 'http://localhost:8080'
CAS_ORCID_REVOKE_SHARED_SECRET = os.environ.get('CAS_ORCID_REVOKE_SHARED_SECRET', 'changeme')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this one

@cslzchen
cslzchen changed the base branch from feature/pbs-26-15 to feature/gdpr-delete-orcid-rewrite August 28, 2026 13:09

@cslzchen cslzchen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3rd pass done 🌟

This is the main PR now, please update ticket title.

Comment thread osf/models/user.py
Comment on lines +326 to +328
# <external_id>: {
# "access_token" : <token>,
# }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to WAR-Overlay the apereo & pac4j library (see here) to include refresh token. Please add the refresh token back.

Comment thread osf/models/user.py
Comment on lines +2156 to +2157
# 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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please write a small python code snippet for CE to run to check if we have any users with more than one ORCiD ID (via cloud request).

Comment thread osf/models/user.py
'''
# 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, add refresh_token back and make it optional (just in case some user's privacy settings in ORCiD doesn't release refresh token to us.

Comment thread osf/models/user.py
raise UserStateError(
'User do not have connected ORCID'
)
else:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for else since error would have been raised

import osf.utils.fields


class Migration(migrations.Migration):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to 0053_gdpr_delete_and_orcid_revoke

Comment thread framework/auth/cas.py
Comment on lines +260 to +264
sentry.log_message(
f'CAS response ORCID attributes: user=[{user._id}], orcidId=[{orcid_id}], '
f'orcidAccessToken=[{"present" if access_token else "missing"}]',
level=logging.WARNING,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future (new ticket), change this to debug log and only log empty access token error after we dev tested on staging1.

Comment thread framework/auth/cas.py
Comment on lines +270 to +274
sentry.log_message(
f'ORCID token stored on external_identity_tokens: user=[{user._id}], '
f'provider_id=[{orcid_id}], access_token=[{"present" if access_token else "missing"}]',
level=logging.INFO,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

Comment thread framework/auth/cas.py
# this extra step will guarantee that 2FA are enforced
# current CAS session created by external login must be cleared first before authentication
if external_credential:
access_token = cas_resp.attributes.get('orcidAccessToken', None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, let's get refresh token as optional.

Comment thread framework/auth/cas.py
user = {
'external_id_provider': external_credential['provider'],
'external_id': external_credential['id'],
'external_id_access_token': cas_resp.attributes.get('orcidAccessToken', None),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

SPAM_SUBMIT_TASK_HARD_TIME_LIMIT = 90

CAS_SERVER_URL = 'http://localhost:8080'
CAS_ORCID_REVOKE_SHARED_SECRET = os.environ.get('CAS_ORCID_REVOKE_SHARED_SECRET', 'changeme')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we still need this one?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants