Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions CHANGES/+self_service_user.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added ability for non-admin users to update their personal account fields
1 change: 1 addition & 0 deletions pulpcore/app/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
GroupSerializer,
GroupUserSerializer,
LoginSerializer,
LoginUpdateSerializer,
NestedRoleSerializer,
RoleSerializer,
UserRoleSerializer,
Expand Down
61 changes: 60 additions & 1 deletion pulpcore/app/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.contrib.auth import get_user_model
from django.contrib.auth import login as auth_login
from django.contrib.auth.models import Permission
from django.contrib.auth.hashers import make_password
from django.contrib.auth.hashers import make_password, check_password
from django.contrib.auth.password_validation import validate_password
from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers
Expand Down Expand Up @@ -548,3 +548,62 @@ def create(self, validated_data):
user = self.context["request"].user
auth_login(self.context["request"], user)
return user


class LoginUpdateSerializer(serializers.ModelSerializer):
username = serializers.CharField(
help_text=_("150 characters or fewer. Letters, digits and @/./+/-/_ only."),
max_length=150,
validators=[UniqueValidator(queryset=User.objects.all())],
required=False,
)
old_password = serializers.CharField(
help_text=_("Old password"),
write_only=True,
required=False,
style={"input_type": "password"},
)
new_password = serializers.CharField(
help_text=_("New password"),
write_only=True,
required=False,
style={"input_type": "password"},
)
first_name = serializers.CharField(
help_text=_("First name"),
max_length=150,
allow_blank=True,
required=False,
)
last_name = serializers.CharField(
help_text=_("Last name"),
max_length=150,
allow_blank=True,
required=False,
)
email = serializers.EmailField(
help_text=_("Email address"),
allow_blank=True,
required=False,
)

def validate(self, data):
data = super().validate(data)
if "new_password" in data and "old_password" not in data:
raise serializers.ValidationError(_("Old password is required to update the password."))
if "new_password" in data and "old_password" in data:
old_password = data.pop("old_password")
new_password = data.pop("new_password")
if not check_password(old_password, self.context["request"].user.password):
raise serializers.ValidationError(_("Old password is incorrect."))
if new_password == old_password:
raise serializers.ValidationError(
_("New password cannot be the same as the old password.")
)
validate_password(new_password)
data["password"] = make_password(new_password)
return data

class Meta:
model = User
fields = ("username", "old_password", "new_password", "first_name", "last_name", "email")
15 changes: 15 additions & 0 deletions pulpcore/app/viewsets/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
GroupUserSerializer,
GroupRoleSerializer,
LoginSerializer,
LoginUpdateSerializer,
RoleSerializer,
UserSerializer,
UserRoleSerializer,
Expand Down Expand Up @@ -453,6 +454,20 @@ def delete(self, request):
auth_logout(request)
return Response(status=204)

@extend_schema(
operation_id="login_update",
request=LoginUpdateSerializer,
responses={200: LoginUpdateSerializer},
)
def patch(self, request):
instance = request.user
serializer = LoginUpdateSerializer(
instance, data=request.data, context={"request": request}, partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)


# Annotate without redefining the post method.
extend_schema(operation_id="login")(LoginViewSet.post)
66 changes: 66 additions & 0 deletions pulpcore/tests/functional/api/test_users_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,72 @@ def test_filter_users(pulpcore_bindings, gen_user):
assert len(users.results) == 1


@pytest.mark.parallel
def test_user_self_service(pulpcore_bindings, gen_user):
"""Test that users can update their own user information."""
prefix = str(uuid.uuid4())
user = gen_user(username=f"{prefix}_newbee")
pro_user = gen_user(username=f"{prefix}_pro")
new_body = {
"email": "test@example.com",
"first_name": "Test",
"last_name": "User",
}
with user:
user_updated = pulpcore_bindings.LoginApi.login_update(new_body)
assert user_updated.email == new_body["email"]
assert user_updated.first_name == new_body["first_name"]
assert user_updated.last_name == new_body["last_name"]

with pytest.raises(pulpcore_bindings.ApiException) as exc:
pulpcore_bindings.LoginApi.login_update({"username": pro_user.username})
assert exc.value.status == 400
assert "This field must be unique" in exc.value.body

with pytest.raises(pulpcore_bindings.ApiException) as exc:
pulpcore_bindings.UsersApi.read(user.user.pulp_href)
assert exc.value.status == 403

user_read = pulpcore_bindings.UsersApi.read(user.user.pulp_href)
assert user_read.email == user_updated.email
assert user_read.first_name == user_updated.first_name
assert user_read.last_name == user_updated.last_name

# Try password update scenarios
with user:
new_password = str(uuid.uuid4())
with pytest.raises(pulpcore_bindings.ApiException) as exc:
pulpcore_bindings.LoginApi.login_update(
{"old_password": "wrong_password", "new_password": new_password}
)
assert exc.value.status == 400
assert "Old password is incorrect." in exc.value.body

with pytest.raises(pulpcore_bindings.ApiException) as exc:
pulpcore_bindings.LoginApi.login_update(
{"old_password": user.password, "new_password": user.password}
)
assert exc.value.status == 400
assert "New password cannot be the same as the old password." in exc.value.body

with pytest.raises(pulpcore_bindings.ApiException) as exc:
pulpcore_bindings.LoginApi.login_update({"new_password": "1234567890"})
assert exc.value.status == 400
assert "Old password is required to update the password." in exc.value.body

pulpcore_bindings.LoginApi.login_update(
{"old_password": user.password, "new_password": new_password}
)
with pytest.raises(pulpcore_bindings.ApiException) as exc:
pulpcore_bindings.LoginApi.login()
assert exc.value.status == 401
assert "Invalid username/password" in exc.value.body
user.password = new_password
with user:
user_read = pulpcore_bindings.LoginApi.login_read()
assert user_read.username == user.username


@pytest.mark.parallel
def test_crd_groups(pulpcore_bindings, gen_object_with_cleanup):
"""Test that a group can be crd."""
Expand Down
Loading