Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions awx/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,7 @@ class UserSerializer(BaseSerializer):
password = serializers.CharField(required=False, default='', help_text=_('Field used to change the password.'))
is_system_auditor = serializers.BooleanField(default=False)
show_capabilities = ['edit', 'delete']
password_reset_required = serializers.BooleanField(default=False, help_text=_('Force the user to reset their password on next login.'))

class Meta:
model = User
Expand All @@ -1039,6 +1040,7 @@ class Meta:
'is_superuser',
'is_system_auditor',
'password',
'password_reset_required',
'last_login',
)
extra_kwargs = {'last_login': {'read_only': True}}
Expand Down Expand Up @@ -1139,6 +1141,7 @@ def _update_password(self, obj, new_password):
def create(self, validated_data):
new_password = validated_data.pop('password', None)
is_system_auditor = validated_data.pop('is_system_auditor', None)
password_reset_required = validated_data.pop('password_reset_required', False)
obj = super(UserSerializer, self).create(validated_data)
self._update_password(obj, new_password)
if is_system_auditor is not None:
Expand All @@ -1148,6 +1151,7 @@ def create(self, validated_data):
def update(self, obj, validated_data):
new_password = validated_data.pop('password', None)
is_system_auditor = validated_data.pop('is_system_auditor', None)
password_reset_required = validated_data.pop('password_reset_required', False)
obj = super(UserSerializer, self).update(obj, validated_data)
self._update_password(obj, new_password)
if is_system_auditor is not None:
Expand Down
13 changes: 13 additions & 0 deletions awx/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,19 @@ class UserDetail(RetrieveUpdateDestroyAPIView):
serializer_class = serializers.UserSerializer
resource_purpose = 'user detail'

def get(self, request, *args, **kwargs):
obj = self.get_object()

# If the admin set the 'password_reset_required' flag to True
if getattr(obj, 'password_reset_required', False):
# We return a 403 Forbidden with a specific detail message
return Response({
"detail": _("Password reset is required before you can continue."),
"password_reset_required": True
}, status=status.HTTP_403_FORBIDDEN)

return super(UserDetail, self).get(request, *args, **kwargs)

def update_filter(self, request, *args, **kwargs):
'''make sure non-read-only fields that can only be edited by admins, are only edited by admins'''
obj = self.get_object()
Expand Down
7 changes: 7 additions & 0 deletions awx/conf/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ class SettingCategoryList(ListAPIView):

@extend_schema_if_available(extensions={"x-ai-description": "A list of additional API endpoints related to settings."})
def get(self, request, *args, **kwargs):
user = request.user
# If the database flag is set to True, tell the UI to block the user
if getattr(user, 'password_reset_required', False):
return Response({
"detail": "Password reset is required before proceeding.",
"password_reset_required": True
}, status=status.HTTP_403_FORBIDDEN)
return super().get(request, *args, **kwargs)

def get_queryset(self):
Expand Down
16 changes: 16 additions & 0 deletions awx/main/migrations/0205_add_password_reset_flag.py
Copy link
Member

Choose a reason for hiding this comment

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

I don't actually believe this works. The model is auth.User, meaning that it comes from the django.contrib app. And this migration is for the main app, basically the AWX app.

The User model being in an app we don't control has been a major thorn in our sides for a long time. But it is difficult to change.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for catching that, @AlanCoding. You're right trying to migrate a field into django.contrib.auth from the main app is asking for trouble.
To avoid fighting the built-in User model, I'll pivot to creating a UserProfile (or similar) model in the main app with a OneToOneField to User. This keeps the migration within our control while still allowing us to track the password_reset_required flag. I’ll update the enforcement logic to check user.profile.password_reset_required instead.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.db import migrations, models

class Migration(migrations.Migration):

dependencies = [
# Replace '0125_some_name' with the ACTUAL name of the last file in the folder
('main', '0125_some_name'),
]

operations = [
migrations.AddField(
model_name='user',
name='password_reset_required',
field=models.BooleanField(default=False, help_text='Force the user to reset their password on next login.'),
),
]
2 changes: 2 additions & 0 deletions awx/main/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.conf import settings # noqa
from django.db import connection
from django.db.models.signals import pre_delete # noqa
from django.db import models # for pass

# django-ansible-base
from ansible_base.resource_registry.fields import AnsibleResourceField
Expand Down Expand Up @@ -186,6 +187,7 @@ def get_system_auditor_role():
rd.permissions.add(*list(permission_registry.permission_qs.filter(codename__startswith='view')))
return rd

User.add_to_class('password_reset_required', models.BooleanField(default=False))

@property
def user_is_system_auditor(user):
Expand Down