diff --git a/core/serializers/common_serializers.py b/core/serializers/common_serializers.py
index badfb88f7555fde47b8c0fd2694da6a4c440c92e..305b2c6cb2cad6f368cc547f9ea9ee00d210a686 100644
--- a/core/serializers/common_serializers.py
+++ b/core/serializers/common_serializers.py
@@ -1,5 +1,9 @@
 import logging
 
+import django.contrib.auth.password_validation as validators
+from django.core import exceptions
+from rest_framework import serializers
+
 from core import models
 
 from .generic import DynamicFieldsModelSerializer
@@ -39,3 +43,23 @@ class SubmissionTypeSerializer(SubmissionTypeListSerializer):
                   'description',
                   'solution',
                   'programming_language')
+
+
+class UserAccountSerializer(DynamicFieldsModelSerializer):
+
+    def validate(self, data):
+        password = data.get('password')
+
+        try:
+            if password is not None:
+                validators.validate_password(password=password,
+                                             user=self.instance)
+        except exceptions.ValidationError as err:
+            raise serializers.ValidationError({'password': list(err.messages)})
+        return data
+
+    class Meta:
+        model = models.UserAccount
+        fields = ('pk', 'username', 'role', 'is_admin', 'password')
+        read_only_fields = ('pk', 'username', 'role', 'is_admin')
+        extra_kwargs = {'password': {'write_only': True}}
diff --git a/core/tests/test_user_account_views.py b/core/tests/test_user_account_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..705c6d30380fc1f3fb38cfb6c14dd6a378e020f8
--- /dev/null
+++ b/core/tests/test_user_account_views.py
@@ -0,0 +1,83 @@
+from rest_framework import status
+from rest_framework.test import (APIClient, APITestCase)
+
+from util.factories import GradyUserFactory
+
+
+class TutorReviewerCanChangePasswordTests(APITestCase):
+    @classmethod
+    def setUpTestData(cls):
+        cls.user_factory = GradyUserFactory()
+        cls.data = {
+            'old_password': 'l',
+            'new_password': 'p'
+        }
+
+    def setUp(self):
+        self.reviewer = self.user_factory.make_reviewer(password='l')
+        self.tutor1 = self.user_factory.make_tutor(password='l')
+        self.tutor2 = self.user_factory.make_tutor(password='l')
+        self.client = APIClient()
+
+    def _change_password(self, changing_user, user_to_change=None, data=None):
+        if user_to_change is None:
+            user_to_change = changing_user
+        if data is None:
+            data = self.data
+
+        self.client.force_authenticate(user=changing_user)
+        url = f"/api/user/{user_to_change.pk}/change_password/"
+        return self.client.patch(url, data=data)
+
+    def test_tutor_needs_to_provide_current_password(self):
+        response = self._change_password(self.tutor1,
+                                         data={'new_password': 'p'})
+        self.assertEqual(status.HTTP_401_UNAUTHORIZED, response.status_code)
+        ret = self.client.login(username=self.tutor1.username,
+                                password='p')
+        self.assertFalse(ret)
+
+    def test_reviewer_needs_to_provide_current_password_for_self(self):
+        response = self._change_password(self.reviewer,
+                                         data={'new_password': 'p'})
+        self.assertEqual(status.HTTP_401_UNAUTHORIZED, response.status_code)
+        ret = self.client.login(username=self.tutor1.username,
+                                password='p')
+        self.assertFalse(ret)
+
+    def test_tutor_can_change_own_password(self):
+        response = self._change_password(self.tutor1)
+        self.assertEqual(status.HTTP_200_OK, response.status_code)
+        ret = self.client.login(username=self.tutor1.username,
+                                password='p')
+        self.assertTrue(ret)
+
+    def test_tutor_cant_change_other_password(self):
+        response = self._change_password(self.tutor1, self.tutor2)
+        self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code)
+        ret = self.client.login(username=self.tutor2.username,
+                                password='p')
+        self.assertFalse(ret)
+
+    def test_reviewer_can_change_own_password(self):
+        response = self._change_password(self.reviewer)
+        self.assertEqual(status.HTTP_200_OK, response.status_code)
+        ret = self.client.login(username=self.reviewer.username,
+                                password='p')
+        self.assertTrue(ret)
+
+    def test_reviewer_can_change_tutor_password(self):
+        response = self._change_password(self.reviewer, self.tutor1,
+                                         data={'new_password': 'p'})
+        self.assertEqual(status.HTTP_200_OK, response.status_code)
+        ret = self.client.login(username=self.tutor1.username,
+                                password='p')
+        self.assertTrue(ret)
+
+    def test_student_cant_change_password(self):
+        student = self.user_factory.make_student(password='l')
+        response = self._change_password(student)
+        self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code)
+        ret = self.client.login(username=student.username,
+                                password='p')
+        self.assertFalse(ret)
diff --git a/core/urls.py b/core/urls.py
index 715379d0180a2e4d97bb6d89cddb268d87d80817..6772c660a2c57b4babb356f36c9b2f643dc86c50 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -18,6 +18,7 @@ router.register('subscription', views.SubscriptionApiViewSet,
                 base_name='subscription')
 router.register('assignment', views.AssignmentApiViewSet)
 router.register('statistics', views.StatisticsEndpoint, base_name='statistics')
+router.register('user', views.UserAccountViewSet, base_name='user')
 
 # regular views that are not viewsets
 regular_views_urlpatterns = [
diff --git a/core/views/common_views.py b/core/views/common_views.py
index 5a28fbc84b0b16af0824f6252b6fea4ebd6c5c75..c3201c806958a8d316e318659d6901b5c0b79ced 100644
--- a/core/views/common_views.py
+++ b/core/views/common_views.py
@@ -4,9 +4,14 @@ user to be authenticated and most are only accessible by one user group """
 import logging
 
 from django.conf import settings
-from django.db.models import Avg
+from django.contrib.auth.hashers import check_password
+from django.db.models import Avg, Q
+import django.contrib.auth.password_validation as validators
+from django.core import exceptions
+
 from rest_framework import generics, mixins, status, viewsets
-from rest_framework.decorators import api_view, list_route, throttle_classes
+from rest_framework.decorators import (api_view, list_route, throttle_classes,
+                                       detail_route)
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.permissions import AllowAny
 from rest_framework.response import Response
@@ -18,7 +23,8 @@ from core.permissions import IsReviewer, IsStudent, IsTutorOrReviewer
 from core.serializers import (ExamSerializer, StudentInfoSerializer,
                               StudentInfoSerializerForListView,
                               SubmissionNoTypeSerializer, SubmissionSerializer,
-                              SubmissionTypeSerializer, TutorSerializer)
+                              SubmissionTypeSerializer, TutorSerializer,
+                              UserAccountSerializer)
 
 log = logging.getLogger(__name__)
 
@@ -97,7 +103,7 @@ class TutorApiViewSet(
         mixins.DestroyModelMixin,
         mixins.ListModelMixin,
         viewsets.GenericViewSet):
-    """ Api endpoint for creating, listing, viewing or deleteing tutors """
+    """ Api endpoint for creating, listing, viewing or deleting tutors """
     permission_classes = (IsReviewer,)
     queryset = models.UserAccount.tutors \
         .with_feedback_count() \
@@ -165,3 +171,43 @@ class SubmissionViewSet(viewsets.ReadOnlyModelViewSet):
         return self.queryset.filter(
             assignments__subscription__owner=self.request.user
         )
+
+
+class UserAccountViewSet(viewsets.ReadOnlyModelViewSet):
+    serializer_class = UserAccountSerializer
+    queryset = models.UserAccount.objects.all()
+
+    @detail_route(methods=['patch'], permission_classes=(IsTutorOrReviewer, ))
+    def change_password(self, request, *args, **kwargs):
+        user = self.get_object()
+        if request.user != user and not request.user.is_reviewer():
+            return Response(status=status.HTTP_403_FORBIDDEN)
+        old_password = request.data.get('old_password')
+
+        # tutors must always provide their current password
+        # reviewers must provide their current password when they change
+        # their own, not if they change the password of a tutor
+        if (request.user.is_tutor() or
+            request.user.is_reviewer and request.user == user) \
+            and \
+            (old_password is None or
+                not check_password(old_password, user.password)):
+                    return Response(status=status.HTTP_401_UNAUTHORIZED)
+
+        new_password = request.data.get('new_password')
+        # validate password
+        try:
+            if new_password is not None:
+                validators.validate_password(password=new_password, user=user)
+        except exceptions.ValidationError as err:
+            return Response({'new_password': list(err.messages)},
+                            status=status.HTTP_406_NOT_ACCEPTABLE)
+        user.set_password(new_password)
+        user.save()
+        log.info(f"User {request.user} changed password of {user}")
+        return Response(status=status.HTTP_200_OK)
+
+    @list_route()
+    def me(self, request):
+        serializer = self.get_serializer(request.user)
+        return Response(serializer.data, status=status.HTTP_200_OK)