diff --git a/docs/README.md b/docs/README.md index 19fd0f9c..ae920412 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,7 @@ - [Development Setup Notes](./development_setup_notes.md) - [Environment Variables](./env_vars.md) - [Example Files](./examples/README.md) - Contains samples of `libraries.json`. `.gitmodules`, and other files that Boost data depends on -- [Handling "Unclaimed" User Accounts](./unclaimed_user_accounts.md) - Describes how we allow authors and maintainers to "claim" the accounts that we create for them as part of the library upload process +- [User Management](./user_management.md) - Describes how we allow authors and maintainers to "claim" the accounts that we create for them as part of the library upload process, and how to prevent users from updating their own profile photos. - [Management Commands](./commands.md) - [Retrieving Static Content from the Boost Amazon S3 Bucket](./static_content.md) - [Syncing Data about Boost Versions and Libraries with GitHub](./syncing_data_with_github.md) diff --git a/docs/unclaimed_user_accounts.md b/docs/user_management.md similarity index 55% rename from docs/unclaimed_user_accounts.md rename to docs/user_management.md index 0ba04cb4..1fe26f0b 100644 --- a/docs/unclaimed_user_accounts.md +++ b/docs/user_management.md @@ -1,24 +1,37 @@ -# Handling "Unclaimed" User Accounts +# User Management +## Handling "Unclaimed" User Accounts -This page covers how to handle the User records that are created as part of the upload process from the [first-time data import](./first_time_data_import.md) and through [syncing with GitHub](./syncing_data_with_github.md). +This section covers how to handle the User records that are created as part of the upload process from the [first-time data import](./first_time_data_import.md) and through [syncing with GitHub](./syncing_data_with_github.md). The code for this page lives in the `users/` app. -## About Registration and Login +### About Registration and Login - We use Django Allauth to handle user accounts - We do some overriding of their logic for the profile page - We override the password reset logic as part of allowing users to claim their unclaimed accounts -## About Unclaimed User Accounts +### About Unclaimed User Accounts - When libraries are created and updated from GitHub, we receive information on Library Maintainers and Authors. - Those authors and maintainers are added as Users and then linked to the Library or LibraryVersion record they belong to. - When they are created, the User accounts have their `claimed` field marked as False. This field defaults to `True`, and will only be `False` for users who were created by an automated process. - We use the email address and name in the `libraries.json` file for that library to create the User record -## When An Unclaimed User Tries to Register +### When An Unclaimed User Tries to Register - If a user tries to register with the same email address as an existing user (a user with `claimed` set to `False`), we interrupt the Django Allauth registration error that happens in this case to check whether the user has been claimed - If the user has `claimed` set to `False`, we send the user a custom message and send them a password reset email - On the backend, we interrupt the Django Allauth password reset process to mark users as claimed once their password has been successfully reset + +## Preventing a user from being able to update their profile photo + +To guard against bad actors uploading offensive photos as profile photos, we have the ability to restrict a user's ability to update their profile photo. + +1. Log into the Django admin at `/admin/` +2. Navigate to the Users menu +3. Find the user whose profile photo you want to restrict. You can search by name or email address. +4. Scroll to the bottom of that user's record and uncheck the box that says, "Can update image." +5. Click "Save." + +The user can no longer update their own profile photo. Instead, they will see a message that they must contact an administrator to update their profile photo. diff --git a/templates/users/profile.html b/templates/users/profile.html index 0a545687..deb6527e 100644 --- a/templates/users/profile.html +++ b/templates/users/profile.html @@ -48,28 +48,32 @@

{% trans "Update Profile Photo" %}

- {% if user.github_username %} -
- {% csrf_token %} - -
- {% endif %} + {% if can_update_image %} + {% if user.github_username %} +
+ {% csrf_token %} + +
+ {% endif %} -
- {% csrf_token %} - {% for field in profile_photo_form.visible_fields %} -
- {% include "includes/_form_input.html" with form=profile_photo_form field=field %} + + {% csrf_token %} + {% for field in profile_photo_form.visible_fields %} +
+ {% include "includes/_form_input.html" with form=profile_photo_form field=field %} +
+ {% endfor %} +
+
- {% endfor %} -
- -
- + + {% else %} +
Please contact an administrator to update your profile photo.
+ {% endif %}
diff --git a/users/admin.py b/users/admin.py index e8df950d..e5e4596a 100644 --- a/users/admin.py +++ b/users/admin.py @@ -34,7 +34,15 @@ class EmailUserAdmin(UserAdmin): ), (_("Important dates"), {"fields": ("last_login", "date_joined")}), (_("Data"), {"fields": ("data",)}), - (_("Image"), {"fields": ("image",)}), + ( + _("Image"), + { + "fields": ( + "can_update_image", + "image", + ) + }, + ), ) add_fieldsets = ( (None, {"classes": ("wide",), "fields": ("email", "password1", "password2")}), diff --git a/users/forms.py b/users/forms.py index 862e44ae..c9bedcb6 100644 --- a/users/forms.py +++ b/users/forms.py @@ -84,3 +84,13 @@ class UserProfilePhotoForm(forms.ModelForm): class Meta: model = User fields = ["image"] + + def clean(self): + """Ensure a user can't update their photo if they + don't have permission.""" + cleaned_data = super().clean() + if not self.instance.can_update_image: + raise forms.ValidationError( + "You do not have permission to update your profile photo." + ) + return cleaned_data diff --git a/users/migrations/0012_user_can_update_image.py b/users/migrations/0012_user_can_update_image.py new file mode 100644 index 00000000..26c26991 --- /dev/null +++ b/users/migrations/0012_user_can_update_image.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.2 on 2023-11-03 17:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0011_alter_user_image"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="can_update_image", + field=models.BooleanField( + default=True, + help_text="Designates whether the user can update their profile photo. To turn off a user's ability to update their own profile photo, uncheck this box.", + verbose_name="can_update_image", + ), + ), + ] diff --git a/users/models.py b/users/models.py index 24b3346c..ac8ff3e2 100644 --- a/users/models.py +++ b/users/models.py @@ -225,6 +225,14 @@ class User(BaseUser): ), ) display_name = models.CharField(max_length=255, blank=True, null=True) + can_update_image = models.BooleanField( + _("can_update_image"), + default=True, + help_text=_( + "Designates whether the user can update their profile photo. To turn off " + "a user's ability to update their own profile photo, uncheck this box." + ), + ) def save_image_from_github(self, avatar_url): response = requests.get(avatar_url) diff --git a/users/views.py b/users/views.py index f83a9ff5..b1c2c2bc 100644 --- a/users/views.py +++ b/users/views.py @@ -82,6 +82,7 @@ class CurrentUserProfileView(LoginRequiredMixin, SuccessMessageMixin, TemplateVi context["change_password_form"] = ChangePasswordForm(user=self.request.user) context["profile_form"] = UserProfileForm(instance=self.request.user) context["profile_photo_form"] = UserProfilePhotoForm(instance=self.request.user) + context["can_update_image"] = self.request.user.can_update_image context["profile_preferences_form"] = PreferencesForm( instance=self.request.user.preferences )