Skip to content
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

Add ConnectID Invite/De-Link/Re-Link features #34967

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open

Conversation

sravfeyn
Copy link
Member

@sravfeyn sravfeyn commented Aug 12, 2024

Product Description



Screenshot 2024-08-12 at 5 06 52 PM
Screenshot 2024-08-12 at 5 07 12 PM

Technical Summary

This provides UI actions to provision users using ConnectID auth backend implemented previously.

Feature Flag

COMMCARE_CONNECT

Safety Assurance

Safety story

I have tested this locally and it's within a feature flag

Automated test coverage

QA Plan

Todo

Migrations

  • The migrations in this code can be safely applied first independently of the code

Rollback instructions

  • This PR can be reverted after deploy with no further considerations

Labels & Review

  • Risk label is set correctly
  • The set of people pinged as reviewers is appropriate for the level of risk of the change

@sravfeyn sravfeyn requested a review from esoergel as a code owner August 12, 2024 11:34
@dimagimon dimagimon added the reindex/migration Reindex or migration will be required during or before deploy label Aug 12, 2024
@sravfeyn sravfeyn requested a review from calellowitz August 12, 2024 11:34
messages.error(request, "The user is already confirmed or is not a mobile user")
return HttpResponse(status=400)

invite_code = encrypt_account_confirmation_info(user)
Copy link
Member Author

Choose a reason for hiding this comment

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

Todo; I will need to coordinate with mobile to get a link that opens ConnectID app with the invite intent.

Copy link
Contributor

Choose a reason for hiding this comment

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

One other alternative is to send this as push notification (probably via the connectid server). That way the app is already handling it, and we know they are logged in. It will also allow us to hide all the info that doesn't matter to the user (like the code or username, which they won't be familiar with), since we can pass that in the data params.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, definitely push notification would be better.

I remember Dave mentioning this specifically needed an SMS deeplink (I don't recall why). I will check with him and see if we can do push notification (on top of SMS).

Copy link
Contributor

@calellowitz calellowitz left a comment

Choose a reason for hiding this comment

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

general idea looks good. added a few questions and suggestions

@@ -832,10 +840,15 @@ def __init__(self, project, request_user, *args, **kwargs):
data_bind='value: send_account_confirmation_email',
)

if TWO_STAGE_USER_PROVISIONING_BY_SMS.enabled(self.domain):
# cid => connect-id
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: might be better to just have connect_id in the variable than use cid and add a comment to explain it

) or toggles.TWO_STAGE_USER_PROVISIONING_BY_SMS.enabled(self.domain)
return (toggles.TWO_STAGE_USER_PROVISIONING.enabled(self.domain)
or toggles.TWO_STAGE_USER_PROVISIONING_BY_SMS.enabled(self.domain)
or toggles.COMMCARE_CONNECT.enabled(self.domain))
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this require stage confirmation for connect domains or just allow it?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a context variable that's used in the template for things that are common for all two-stage provision workflows.

@require_can_edit_commcare_users
@require_POST
@location_safe
def activate_connectid_link(request, domain, user_id):
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: if it is a POST anyway these views can be combined and just pass the is_active value in the payload, but definitely not blocking and fine to keep it this way

Copy link
Member Author

Choose a reason for hiding this comment

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

True. But there is a tradeoff on JS side. The corresponding JS here which is also used for regular (de/)activate needs to handle the POST param for connectid separately. So it's not much worth.


def _toggle_connectid_link(request, domain, user_id, is_active):
if not toggles.COMMCARE_CONNECT.enabled(domain):
return HttpResponseBadRequest()
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: i think this should be a 403 and can be added as a permission check to the views (the request isn't bad, they just aren't allowed to do it)

def _toggle_connectid_link(request, domain, user_id, is_active):
if not toggles.COMMCARE_CONNECT.enabled(domain):
return HttpResponseBadRequest()
user = CommCareUser.get_by_user_id(user_id, domain)
Copy link
Contributor

Choose a reason for hiding this comment

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

is it possible to pass the username in the view so that we can fetch the django user directly rather than the couch user and then the django user?

Copy link
Member Author

Choose a reason for hiding this comment

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

The tradeoff is that we lookup and send django user IDs for all users in the paginate_mobile_workers so that the caller knows the django userid to pass it to this view.

commcare_user__username__in=usernames
).annotate(commcare_username=F("commcare_user__username")).all()
return {
link.commcare_username.split("@")[0]: link
Copy link
Contributor

Choose a reason for hiding this comment

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

do we have a util for this somewhere? I realize its a small thing, but we do it so much in HQ that I am surprised we dont have a function for it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I thought the same, I couldn't find either. Most places do this same thing, and perhaps it wasn't worth a util that has just this one line in it.

Copy link
Contributor

Choose a reason for hiding this comment

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

corehq.apps.users.util.raw_username

domain: {domain}
invite_code: {invite_code}
url: {url}
Make a POST call to above url with invite_code and your ConnectID token
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we want to send the user a message telling them to make a POST, because they won't know what to do with that info. I think we want to pass the right info to the phone so it can do that automatically (or have a clickable link that will confirm it, but a link that is clickable in the sms but doesn't do the right thing is going to be confusing)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, definitely, this is a ToDo; mobile team has given me the correct format, so I will update this.

messages.error(request, "The user is already confirmed or is not a mobile user")
return HttpResponse(status=400)

invite_code = encrypt_account_confirmation_info(user)
Copy link
Contributor

Choose a reason for hiding this comment

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

One other alternative is to send this as push notification (probably via the connectid server). That way the app is already handling it, and we know they are logged in. It will also allow us to hide all the info that doesn't matter to the user (like the code or username, which they won't be familiar with), since we can pass that in the data params.

messages.error(request, "The user is already confirmed or is not a mobile user")
return HttpResponse(status=400)

invite_code = encrypt_account_confirmation_info(user)
Copy link
Contributor

Choose a reason for hiding this comment

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

what does encrypting the invite code do for us here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think the code 'encodes' the invitation sent time, so that before confirming it can check the invite didn't expire. This is really a soft enforcement used in the other two multi-stage user provision workflows, so I took the pattern as it is.

if user.domain != domain:
return HttpResponseBadRequest("Invalid Request")

connectid_username = get_connectid_userinfo(token)
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this confirm that the token was valid at all? What response do we send if it wasn't?

Copy link
Member Author

Choose a reason for hiding this comment

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

If the token is not valid this would not return a username. So right now, this would 500 if the token is not valid. Which is same behaviour as that of link_connectid_user.

@dimagimon dimagimon added the Risk: Medium Change affects files that have been flagged as medium risk. label Nov 28, 2024
@sravfeyn
Copy link
Member Author

@calellowitz Bumping for review please

@sravfeyn
Copy link
Member Author

@calellowitz Bumping for review. This, as well as the cchq one dimagi/commcare-cloud#6449

Copy link
Contributor

@calellowitz calellowitz left a comment

Choose a reason for hiding this comment

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

Are you planning to address the lint errors. Code generally looks fine, but that is generally worth doing. Otherwise one question (I see that one of them was corrected in a later commit). Otherwise, I would feel a bit more comfortable if someone outside connect reviewed this, since I am a bit rusty on hq code, but looks good to me.


def send_connectid_invite_sms(user):
if user.is_account_confirmed or not user.is_commcare_user():
messages.error(request, "The user is already confirmed or is not a mobile user")
Copy link
Contributor

Choose a reason for hiding this comment

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

This lint error looks real

# ConnectID server delivers the invite to user
if user.is_account_confirmed or not user.is_commcare_user():
messages.error(request, "The user is already confirmed or is not a mobile user")
if request:
messages.error(request, "The user is already confirmed or is not a mobile user")
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we not want these messages for the import results?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
reindex/migration Reindex or migration will be required during or before deploy Risk: Medium Change affects files that have been flagged as medium risk.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants