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

Expire user permissions #72

Merged
merged 18 commits into from
Nov 10, 2024
Merged

Expire user permissions #72

merged 18 commits into from
Nov 10, 2024

Conversation

mkangia
Copy link
Contributor

@mkangia mkangia commented Nov 1, 2024

https://dimagi.atlassian.net/browse/SC-3475

Building up on work done in cs/SC-3473-user-roles-from-hq to expire user permissions in an hour to keep permissions up to date with HQ.

Review by commit 🐟
There is quite some refactor, done in separate commits for clarity

All tests ran locally successfully. They wont run here since base branch isn't master.

https://dimagi.atlassian.net/browse/QA-7205

@mkangia mkangia requested a review from Charl1996 November 1, 2024 17:08
@mkangia mkangia force-pushed the mk/3475-expire-user-permissions branch from dc39da7 to 12a7dc0 Compare November 3, 2024 09:54
@mkangia mkangia force-pushed the mk/3475-expire-user-permissions branch from 12a7dc0 to c8111d4 Compare November 3, 2024 14:36
@@ -48,3 +49,33 @@ def tearDown(self):
sql = "; ".join(domain_schemas) + ";"
connection.execute(text(sql))
super(HQDBTestCase, self).tearDown()


class LoginUserTestMixin(object):
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice. Seems useful.

@@ -415,3 +418,7 @@ def cast_data_for_table(
def generate_secret():
alphabet = string.ascii_letters + string.digits
return ''.join(secrets.choice(alphabet) for __ in range(64))


def datetime_utcnow_naive():
Copy link
Contributor

Choose a reason for hiding this comment

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

@mkangia
I think this function name is a misnomer. You're returning an aware datetime, are you not?.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right, I got that backwards, will rename this datetime_utcnow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if not session.get(SESSION_DOMAIN_ROLE_LAST_SYNCED_AT):
return True

time_since_last_sync = datetime_utcnow_naive() - session[SESSION_DOMAIN_ROLE_LAST_SYNCED_AT]
Copy link
Contributor

@Charl1996 Charl1996 Nov 4, 2024

Choose a reason for hiding this comment

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

@mkangia
Nit: for more robust implementation we could check if session[SESSION_DOMAIN_ROLE_LAST_SYNCED_AT] is a datetime-like object that could be operated on.

How about:

last_sync_datetime = session[SESSION_DOMAIN_ROLE_LAST_SYNCED_AT]
if not isinstance(last_sync_datetime, datetime):
    return True

time_since_last_sync = datetime_utcnow_naive() - last_sync_datetime

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool. I can add that. This value is only always set by CCA and can't edited by the user so it would certainly be a datetime but no harm in adding safety.

Comment on lines 98 to 99
def _sync_domain_role():
DomainSyncUtil(superset.appbuilder.sm).sync_domain_role(g.hq_domain)
Copy link
Contributor

Choose a reason for hiding this comment

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

@mkangia
What happens if the domain role cannot sync during this routine? Is it fine to keep the user logged in in the current state?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ya, Great question.

I guess I will follow the same pattern you have followed otherwise in case sync domain role returns false and redirect user to the select domain page, though possibly with a different error message like "Your permissions have expired and the sync failed due to an unexpected error. Please select project space again or login to continue"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This turned out to be a little challenging, but I got it working like this
a195929

Screenshot from 2024-11-05 00-58-37

@@ -134,7 +134,7 @@ def _do_assert(datasources):
client.get('/hq_datasource/list/', follow_redirects=True)
_do_assert(self.oauth_mock.test2_datasources)

@patch.object(DomainSyncUtil, "sync_domain_role", return_value=True)
@patch.object(DomainSyncUtil, "_get_domain_access", return_value=({"can_write": True, "can_read": True}, []))
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious what the issue was 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.

It was a flaky test that succeeded when run separately or if only this test file was run but failed when the full test suite was run.
I believe the permissions were getting set unexpectedly or getting revoked somehow, so I set them specifically instead of skipping it via the patch for sync_domain_role. I did try to debug it for a couple of hours before letting it be.

@@ -44,6 +44,7 @@ def after_request_hook(response):
'AuthOAuthView.login',
'AuthOAuthView.logout',
'AuthOAuthView.oauth_authorized',
'CurrentUserRestApi.get_me',
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious what this is for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From the UI, this a call made to constantly sync the user record, I assume its getting used in javascript/UI somewhere.
It is a domain independent API call.
I added it to this list so we don't fire the before request hooks on such requests since they are not needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Found one more, b4eb1b3

@Charl1996
Copy link
Contributor

@mkangia Nice! Thanks for the refactors and good tests!

@mkangia mkangia force-pushed the mk/3475-expire-user-permissions branch from 59370e1 to a5f2c58 Compare November 4, 2024 22:26
from .models import DataSetChange
from .oauth2_server import authorization, require_oauth
from .tasks import process_dataset_change
from hq_superset.models import DataSetChange
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Copy link
Contributor

@ajeety4 ajeety4 left a comment

Choose a reason for hiding this comment

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

Great Refactors. The changes looks good.

Just a query related to the fix made in the commit

  • /api/v1/me is called frequently leading to before_request_hook getting fired unnecessarily, specifically for sync_user_domain_role leading to more than one syncs happening at once leading to db conflicts/errors

Does the db conflicts/errors happened because of API calls that are simultaneous? Wondering if the same thing can occur if a user is working with multiple tabs at once ?

Copy link
Contributor

@Charl1996 Charl1996 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 the changes! Looks good to me!

@mkangia
Copy link
Contributor Author

mkangia commented Nov 6, 2024

Hey @ajeety4

Does the db conflicts/errors happened because of API calls that are simultaneous? Wondering if the same thing can occur if a user is working with multiple tabs at once ?

Spot on. That fix is where I noticed it first.
I made the following change in order to limit to only one sync at once.
It does appear to have solved the issue in most cases.

However there can be some weird edge cases where two requests come exactly at the same moment which I am letting go for now since sync will get reattempted at the next request if needed for such edge cases.
I noticed that locally but not on staging during some minor testing.

@mkangia
Copy link
Contributor Author

mkangia commented Nov 6, 2024

Thanks for the reviews @Charl1996

Base automatically changed from cs/SC-3473-user-roles-from-hq to master November 6, 2024 11:58
@mkangia mkangia merged commit b2c64d3 into master Nov 10, 2024
3 checks passed
@mkangia mkangia deleted the mk/3475-expire-user-permissions branch November 10, 2024 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants