-
Notifications
You must be signed in to change notification settings - Fork 0
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
WIP: Clean user records #482
Draft
marcelkornblum
wants to merge
6
commits into
main
Choose a base branch
from
260-clean-user-records
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
abac8d5
interim
marcelkornblum bc4de98
first stab and stub for mgmt commands
marcelkornblum 48cc1e5
connect to speculatively useful sso endpoint
marcelkornblum 09a3030
mark profiles and users inactive at the same time
marcelkornblum 7166f8e
connect user and profile is_active saving in both directions
marcelkornblum e173e08
try now
marcelkornblum File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
from datetime import timedelta | ||
import requests | ||
from urllib.parse import urlparse, urlunparse | ||
|
||
from django.conf import settings | ||
from django.core.management.base import BaseCommand | ||
from django.utils import timezone | ||
|
||
from user.models import User | ||
from peoplefinder.models import Person | ||
|
||
|
||
class Command(BaseCommand): | ||
help = "Clean up and archive User and Person records" | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument("days", type=int, default=90) | ||
parser.add_argument("limit", type=int, default=25) | ||
parser.add_argument("offset", type=int, default=0) | ||
|
||
def handle(self, *args, **options): | ||
days = options["days"] | ||
limit = options["limit"] | ||
offset = options["offset"] | ||
|
||
self.sync_inactive_users_profiles() | ||
|
||
login_cutoff_date = timezone.now() - timedelta(days=days) | ||
users_to_check = User.objects.filter(is_active=True).filter( | ||
last_login__lt=login_cutoff_date | ||
) | ||
number_of_users = users_to_check.count() | ||
|
||
if limit == 0: | ||
batch = users_to_check | ||
self.stdout.write( | ||
self.style.NOTICE( | ||
f"Checking all {number_of_users} User records with last_login older than {days} ago" | ||
) | ||
) | ||
else: | ||
batch = users_to_check[offset:limit] | ||
self.stdout.write( | ||
self.style.NOTICE( | ||
f"Checking {limit} (from record {offset}) of {number_of_users} User records with last_login older than {days} ago" | ||
) | ||
) | ||
|
||
deactivated = 0 | ||
ignored = 0 | ||
# check against SSO API endpoint to see if user is active there | ||
authbroker_url = urlparse(settings.AUTHBROKER_URL) | ||
url = urlunparse(authbroker_url._replace(path="/introspect/")) | ||
headers = {"Authorization": f"bearer {settings.AUTHBROKER_INTROSPECTION_TOKEN}"} | ||
|
||
for user in batch: | ||
params = {"email": user.email} | ||
response = requests.get(url, params, headers=headers, timeout=5) | ||
if response.status_code == 200: | ||
resp_json = response.json() | ||
if not resp_json["is_active"]: | ||
user.is_active = False | ||
user.save() | ||
deactivated += 1 | ||
else: | ||
ignored += 1 | ||
else: | ||
self.stdout.write( | ||
self.style.ERROR( | ||
f"SSO introspect endpoint returned {response.status_code} status code for {user.email}" | ||
) | ||
) | ||
|
||
self.stdout.write( | ||
self.style.SUCCESS( | ||
"Job finished successfully\n" | ||
f"{deactivated} User records marked as inactive\n" | ||
f"{ignored} User records left as active\n" | ||
) | ||
) | ||
|
||
def sync_inactive_users_profiles(self): | ||
mismatched_users = User.objects.filter(profile__is_active=False).filter( | ||
is_active=True | ||
) | ||
mismatched_users.update(active=False) | ||
self.stdout.write( | ||
self.style.SUCCESS( | ||
f"Updated {mismatched_users.count()} active User records with inactive Person profiles to inactive" | ||
) | ||
) | ||
|
||
mismatched_profiles = Person.objects.filter(is_active=True).filter( | ||
user__is_active=False | ||
) | ||
mismatched_profiles.update(active=False) | ||
self.stdout.write( | ||
self.style.SUCCESS( | ||
f"Updated {mismatched_profiles.count()} active Person records with inactive User records to inactive" | ||
) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from django.core.management.base import BaseCommand | ||
from django.db.models import Count | ||
|
||
from user.models import User | ||
from peoplefinder.models import Person | ||
|
||
|
||
class Command(BaseCommand): | ||
help = "Clean up User and Person records" | ||
|
||
def handle(self, *args, **options): | ||
users_without_profile = User.objects.filter(profile__isnull=True) | ||
self.do_delete(users_without_profile, "User") | ||
|
||
profiles_without_user = Person.objects.filter(user__isnull=True) | ||
self.do_delete(profiles_without_user, "Person") | ||
|
||
no_duplicate_emails = ( | ||
User.objects.values("email") | ||
.annotate(email_count=Count("email")) | ||
.filter(email_count__gt=1) | ||
.count() | ||
) | ||
if no_duplicate_emails > 0: | ||
self.stdout.write( | ||
self.style.NOTICE( | ||
f"Found {no_duplicate_emails} User records with duplicate emails" | ||
) | ||
) | ||
|
||
def do_delete(self, queryset, model_name): | ||
self.stdout.write( | ||
self.style.NOTICE( | ||
f"Found {queryset.count()} {model_name} records with no associated Person profile" | ||
) | ||
) | ||
try: | ||
queryset.delete() | ||
self.stdout.write( | ||
self.style.SUCCESS("Batch {model_name} deletion successful") | ||
) | ||
except Exception as e: | ||
self.stderr.write( | ||
self.style.ERROR(f"Batch {model_name} deletion stopped with error: {e}") | ||
) | ||
count = 0 | ||
for record in queryset: | ||
try: | ||
record.delete() | ||
count += 1 | ||
except Exception as e: | ||
self.stderr.write( | ||
self.style.ERROR( | ||
f"{model_name} {record} deletion raised error: {e}" | ||
) | ||
) | ||
self.stdout.write( | ||
self.style.SUCCESS( | ||
"One-by-one deletion successful for {count} {model_name} records" | ||
) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
If the profile is set to active and the user is set to inactive, it will update the user to be active, is this the desired behaviour?
Is there any way that updates the User is active flag? (maybe from SSO?)
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.
I put this as provocation - my assumption is whichever record (User / Person) is being saved, you probably want the other record's is_active to match.... ?
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.
the sso connection is waiting on a erlated PR but is there in the management command; I could abstract this to a service