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

Task to auto deactivate removed sso users #34151

Merged
merged 61 commits into from
Apr 24, 2024

Conversation

jingcheng16
Copy link
Contributor

Product Description

Technical Summary

Ticket: https://dimagi.atlassian.net/browse/SAAS-15233
Tech spec: https://docs.google.com/document/d/1LOF1lCl3wTsEQkEEsHtZi3u4N6kvjXaLv-Lu6e9aYjo/edit#heading=h.ll7lzqwsaw9w

This task will iterate through all IdentityProvider objects where enable_user_deactivation is True. For now we will also filter by idp_type equal to IdentityProviderType.AZURE_AD.

This task will query the Microsoft Graph Users API to fetch a list of all users that are members of the Azure AD Tenant that uses SSO to grant access to CommCare HQ. This task will fetch a list of usernames (email addresses) based on the userPrincipalName field returned by the Microsoft Graph Users API. It will then fetch a list of active WebUser usernames that are members of project spaces associated with the BillingAccount owner of the IdentityProvider. This task will then compare the two lists.

If a user is present in the list of active Web Users on HQ but is not present in the list returned by the Microsoft API query, we will deactivate that user by setting the is_active field on the WebUser to False. Any users that are in the “SSO Exempt” users list should not be automatically deactivated.

It is essential to handle errors in this task in a way that communicates issues swiftly both to the support team and the enterprise admins. For instance, if we are unable to access the Microsoft Graph API, we should raise the error to Sentry and send an email to the enterprise admins that we are having difficulty connecting with the Microsoft Graph API.

Additionally, if the Graph Users API returns an empty list of users, we should consider that an errored state and hold off from automatically deactivating Web Users if the number of active Web Users (that are not SSO Exempt) is greater than 3. This error should also be raised in an email to the enterprise admins alerting that we have temporarily skipped automatic deactivation of Web Users because we are receiving an empty list of Users from Azure AD.

Feature Flag

Safety Assurance

Safety story

I realized this task can be dangerous, we want to make sure we won't accidentally deactivate users that should not be deactivated. I wrote tests for the task, but did I catch all the edge cases?

Automated test coverage

corehq.apps.sso.tests.test_tasks:TestAutoDeactivationTask

QA Plan

No QA at this stage

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

@dimagimon dimagimon added the dependencies Pull requests that update a dependency file label Feb 20, 2024
@jingcheng16 jingcheng16 marked this pull request as ready for review February 20, 2024 21:08
@jingcheng16 jingcheng16 requested a review from a team as a code owner February 20, 2024 21:08
@jingcheng16 jingcheng16 added the product/all-users-all-environments Change impacts all users on all environments label Feb 20, 2024
corehq/apps/sso/models.py Outdated Show resolved Hide resolved
body,
)
else:
users_to_deactivate = [user for user in web_user_in_account if user not in idp_users
Copy link
Member

Choose a reason for hiding this comment

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

what about users that are members of the domains but do not have an email domain controlled by the Identity Provider?

for instance, what if your dimagi user was a member of the domain?

I would like to see tests written for this scenario too :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi Biyeun, thank you for raising this important point! Fixed here: fc918be

@@ -1,3 +1,4 @@
pyOpenSSL>=23.2.0 # for cryptography>=41
cryptography>=41 # for security vulnerability
python3-saml
msal
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
msal
msal

Copy link
Member

Choose a reason for hiding this comment

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

new line at end of file nit

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you Biyeun! Fixed here: a86ca6c (I should just commit suggestion)

@@ -7,5 +7,6 @@ sphinxcontrib-django
sphinx-rtd-theme

pyOpenSSL # CI builds fail without OpenSSL
msal # CI builds fail without OpenSSL
Copy link
Contributor

@millerdev millerdev Feb 26, 2024

Choose a reason for hiding this comment

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

I think this should be added in base-requirements.in and not here. This is not a docs requirement. Also, the comment seems incorrect.

Alternately move the import in models.py into get_all_members_of_the_idp so it does not need to be added here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you Daniel! I go with your second option : 11b1160

# If the user is not returned by IdP and is not exempted from SSO
if user not in idp_users and user not in exempt_usernames:
email_domain = get_email_domain_from_username(user)
authenticated_email_domains = authenticated_domains.values_list('email_domain', flat=True)
Copy link
Member

Choose a reason for hiding this comment

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

does this need to happen within the loop? this only needs to be set once, yes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I moved it: da9df8e

@jingcheng16 jingcheng16 requested a review from biyeun March 1, 2024 16:53
corehq/apps/sso/models.py Outdated Show resolved Hide resolved
Co-authored-by: Biyeun <bbuczyk@dimagi.com>

# Send email
subject = _("Issue Connecting to Microsoft Graph API")
body = f'There was an issue connecting to the Microsoft Graph API: {str(e)}'
Copy link
Member

Choose a reason for hiding this comment

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

we should have a similar template like the email sent here

It should read something like...

Dear enterprise administrator,

There was an issue connecting to the Microsoft Graph API when running a task to deactivate SSO Web Users on CommCare HQ that have been removed from your Microsoft Entra ID Enterprise Application.

This was the error message:
{{ error }}

Please check the configuration of the Remote User Management settings in your Identity Provider.

If you have any questions, please don’t hesitate to contact {{ contact_email }}.
Thank you for your use and support of CommCare.

Best regards,

The CommCare HQ Team

Copy link
Member

Choose a reason for hiding this comment

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

Additionally, the send email portion of this task should probably be its own utility. likewise with the similar suggestion below

Copy link
Contributor Author

@jingcheng16 jingcheng16 Mar 7, 2024

Choose a reason for hiding this comment

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

Thank you Biyeun. I improved it here: f2ed995... I made three mistakes and fixed in 9d29c36 , 9fdd9f1 and bbf62ee

corehq/apps/sso/tasks.py Outdated Show resolved Hide resolved
# Send email
subject = _("Temporarily skipped automatic deactivation of Web Users")
recipient = idp.owner.enterprise_admin_emails
body = _("we have temporarily skipped automatic deactivation of Web Users because we are receiving "
Copy link
Member

Choose a reason for hiding this comment

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

Similar to the previous suggestion for updating the body text in get_all_members_of_the_idp

Dear enterprise administrator,

We have temporarily skipped automatic deactivate of SSO Web Users because we are receiving an empty list of users from your Microsoft Entra ID instance.

Please check the configuration of the Remote User Management settings in your Identity Provider.

If you have any questions, or if this issue persists, please don’t hesitate to contact {{ contact_email }}.
Thank you for your use and support of CommCare.

Best regards,

The CommCare HQ Team

Copy link
Member

Choose a reason for hiding this comment

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

this email bit can also be its own utility abstracted away

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you Biyeun. Improved in 9128522

corehq/apps/sso/models.py Outdated Show resolved Hide resolved
@jingcheng16 jingcheng16 requested review from gherceg and biyeun April 3, 2024 01:35
corehq/apps/sso/utils/context_helpers.py Outdated Show resolved Hide resolved
corehq/apps/sso/utils/context_helpers.py Outdated Show resolved Hide resolved
corehq/apps/sso/utils/entra.py Outdated Show resolved Hide resolved
jingcheng16 and others added 2 commits April 6, 2024 20:02
Co-authored-by: Graham Herceg <gherceg@dimagi.com>
@jingcheng16 jingcheng16 requested a review from gherceg April 7, 2024 02:23
Copy link
Member

@biyeun biyeun left a comment

Choose a reason for hiding this comment

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

great! thank you! 🎉

Copy link
Contributor

@gherceg gherceg left a comment

Choose a reason for hiding this comment

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

Thanks for your patience on this one Jing. Great job!

jingcheng16 and others added 2 commits April 16, 2024 09:54
Co-authored-by: Biyeun <bbuczyk@dimagi.com>
The result returned looks like this: {'error': 'invalid_client',
 'error_description': "AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'f09df325-2ca9-4756-93d4-14934194ae35'. Trace ID: fa552a4f-bb3b-45e0-8432-792489cf1e00 Correlation ID: 379dcf42-fc82-4a36-bce0-d8823be8046b Timestamp: 2024-04-17 14:26:07Z",
 'error_codes': [7000215],
 'timestamp': '2024-04-17 14:26:07Z',
 'trace_id': 'fa552a4f-bb3b-45e0-8432-792489cf1e00',
 'correlation_id': '379dcf42-fc82-4a36-bce0-d8823be8046b',
 'error_uri': 'https://login.microsoftonline.com/error?code=7000215'}
@jingcheng16 jingcheng16 requested a review from biyeun April 17, 2024 21:43
@jingcheng16 jingcheng16 requested review from biyeun and gherceg April 24, 2024 00:00
The batch_result will look like: {'responses': [{'id': '3',
   'status': 403,
   'headers': {'Cache-Control': 'no-cache',
    'x-ms-resource-unit': '1',
    'Content-Type': 'application/json'},
   'body': {'error': {'code': 'Authorization_RequestDenied',
     'message': 'Insufficient privileges to complete the operation.',
     'innerError': {'date': '2024-04-24T15:36:38',
      'request-id': '30dbadf0-2abc-42aa-b838-280a99a477dd',
      'client-request-id': '30dbadf0-2abc-42aa-b838-280a99a477dd'}}}},
  {'id': '1',
   'status': 403,
   'headers': {'Cache-Control': 'no-cache',
    'x-ms-resource-unit': '1',
    'Content-Type': 'application/json'},
   'body': {'error': {'code': 'Authorization_RequestDenied',
     'message': 'Insufficient privileges to complete the operation.',
     'innerError': {'date': '2024-04-24T15:36:38',
      'request-id': '30dbadf0-2abc-42aa-b838-280a99a477dd',
      'client-request-id': '30dbadf0-2abc-42aa-b838-280a99a477dd'}}}},
  {'id': '2',
   'status': 403,
   'headers': {'Cache-Control': 'no-cache',
    'x-ms-resource-unit': '1',
    'Content-Type': 'application/json'},
   'body': {'error': {'code': 'Authorization_RequestDenied',
     'message': 'Insufficient privileges to complete the operation.',
     'innerError': {'date': '2024-04-24T15:36:38',
      'request-id': '30dbadf0-2abc-42aa-b838-280a99a477dd',
      'client-request-id': '30dbadf0-2abc-42aa-b838-280a99a477dd'}}}},
  {'id': '4',
   'status': 403,
   'headers': {'Cache-Control': 'no-cache',
    'x-ms-resource-unit': '1',
    'Content-Type': 'application/json'},
   'body': {'error': {'code': 'Authorization_RequestDenied',
     'message': 'Insufficient privileges to complete the operation.',
     'innerError': {'date': '2024-04-24T15:36:38',
      'request-id': '30dbadf0-2abc-42aa-b838-280a99a477dd',
      'client-request-id': '30dbadf0-2abc-42aa-b838-280a99a477dd'}}}},
  {'id': '0',
   'status': 403,
   'headers': {'Cache-Control': 'no-cache',
    'x-ms-resource-unit': '1',
    'Content-Type': 'application/json'},
   'body': {'error': {'code': 'Authorization_RequestDenied',
     'message': 'Insufficient privileges to complete the operation.',
     'innerError': {'date': '2024-04-24T15:36:38',
      'request-id': '30dbadf0-2abc-42aa-b838-280a99a477dd',
      'client-request-id': '30dbadf0-2abc-42aa-b838-280a99a477dd'}}}}]}
Copy link
Member

@biyeun biyeun left a comment

Choose a reason for hiding this comment

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

retracting approval.
The general error escape shouldn't be sent as an email. those errors we want to use notify_exception and not send the error message raw to the user

notify_exception(None, f"Failed to get members of the IdP. {str(e)}")
send_deactivation_skipped_email(idp=idp, failure_reason=MSGraphIssue.VERIFICATION_ERROR,
error="verification error",
error_description={str(e)})
Copy link
Member

Choose a reason for hiding this comment

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

We should avoid sending the raw error to the user, in the chance it contains sensitive information we didn't expect. notify_exception is fine and necessary, but for this we should say "unknown issue: please contact support"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I see! I replied without seeing this comment. I'll work on an improvement now!

Copy link
Member

Choose a reason for hiding this comment

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

thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi Biyeun, changed in 8871bad

@jingcheng16
Copy link
Contributor Author

@biyeun
The general exception might catch wrong configuration.

    app = msal.ConfidentialClientApplication(
        config["client_id"], authority=config["authority"],
        client_credential=config["secret"],
    )

This initialization can raise ValueError or general exception.

@jingcheng16 jingcheng16 requested a review from biyeun April 24, 2024 17:20
Copy link
Member

@biyeun biyeun left a comment

Choose a reason for hiding this comment

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

thank you!

@jingcheng16 jingcheng16 merged commit b88782e into master Apr 24, 2024
13 checks passed
@jingcheng16 jingcheng16 deleted the jc/auto-deactivate-removed-sso-users branch April 24, 2024 17:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dependencies Pull requests that update a dependency file product/all-users-all-environments Change impacts all users on all environments
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants