django keycloak user deletion

I have integrated Django with Keycloak, and I’ve set the Keycloak user ID as a foreign key for the ‘django_keycloak_user’ table. When I try to delete a user from Keycloak, I encounter an error saying, ‘user does not exist.’ I want to delete the user without affecting any dependent tables.

You’ll need to provide more details here concerning your configuration, along with posting the code you have written to perform this integration. If you’re using the django-keycloak package, identify what version you’re using. Also identify the versions of Python and Django being used.

Note: When posting code here, copy/paste the code into the body of your message, surrounded by lines of three backtick - ` characters. That means you’ll have a line of ```, then your code, then another line of ```.

I have used the django-uw-keycloak 2.0.2 package.
Django = 4.1.3 and python =3.10.12

configuration for keycloak in settings.py

INSTALLED_APPS = ["django_keycloak",]
MIDDLEWARE = ["django_keycloak.middleware.KeycloakMiddleware",]
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "django_keycloak.authentication.KeycloakAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ],
]
KEYCLOAK_CONFIG = {
    # The Keycloak's Public Server URL (e.g. http://localhost:8080)
    "SERVER_URL": KEYCLOAK_SERVER_URL,
    # The Keycloak's Internal URL
    # (e.g. http://keycloak:8080 for a docker service named keycloak)
    # Optional: Default is SERVER_URL
    "INTERNAL_URL": KEYCLOAK_SERVER_URL,
    # Override for default Keycloak's base path
    # Default is '/auth/'
    "BASE_PATH": "/auth/",
    # The name of the Keycloak's realm
    "REALM": KEYCLOAK_REALM,
    # The ID of this client in the above Keycloak realm
    "CLIENT_ID": KEYCLOAK_CLIENT_ID,
    # The secret for this confidential client
    "CLIENT_SECRET_KEY": KEYCLOAK_CLIENT_SECRET_KEY,
    # The name of the admin role for the client
    "CLIENT_ADMIN_ROLE": KEYCLOAK_ADMIN_ROLE,
    # The name of the admin role for the realm
    "REALM_ADMIN_ROLE": KEYCLOAK_REALM_ADMIN_ROLE,
    # Regex formatted URLs to skip authentication
    "EXEMPT_URIS": [],
    # Flag if the token should be introspected or decoded (default is False)
    "DECODE_TOKEN": False,
    # Flag if the audience in the token should be verified (default is True)
    "VERIFY_AUDIENCE": True,
    # Flag if the user info has been included in the token (default is True)
    "USER_INFO_IN_TOKEN": True,
    # Flag to show the traceback of debug logs (default is False)
    "TRACE_DEBUG_LOGS": False,
    # The token prefix that is expected in Authorization header (default is 'Bearer')
    "TOKEN_PREFIX": "Bearer",
}

CELERY_BEAT_SCHEDULE = {
    "sync_users_with_keycloak": {
        "task": "django_keycloak.tasks.sync_users_with_keycloak",
        "schedule": timedelta(hours=2),
        "options": {"queue": f"sync-users-{ENVIRONMENT}"},
    },
}

In the cart model, I have used ForeignKey on user cart.py

from django_keycloak.models import KeycloakUser
class Cart(CMSMixin, TimeStampMixin, models.Model):
    user = models.ForeignKey(
        KeycloakUser, on_delete=models.SET_NULL, null=True, blank=True
    )
```
if I delete the user from Keycloak, error will show **user not found**

Where are you seeing this error? In Keycloak or in your Django project?

If it’s showing up as a Django error, please post the complete traceback. But I’m guessing that a reference to an instance of KeycloakUser that no longer exists is the cause of the error.

You likely want to change your on_delete setting to CASCADE.

the complete traceback

KeycloakGetError at /iam/users/
404: b'{"error":"User not found"}'
Request Method:	GET
Request URL:	http://abc.ser.h.in/iam/users/?ordering=id&page=1&page_size=100
Django Version:	4.2.4
Exception Type:	KeycloakGetError
Exception Value:	
404: b'{"error":"User not found"}'
Exception Location:	/root/.local/lib/python3.10/site-packages/keycloak/exceptions.py, line 192, in raise_error_from_response
Raised during:	iam.views.KeycloakUserViewSet
Python Executable:	/usr/local/bin/python
Python Version:	3.10.13
Python Path:	
['/src',
 '/src',
 '/root/.local/bin',
 '/usr/local/lib/python310.zip',
 '/usr/local/lib/python3.10',
 '/usr/local/lib/python3.10/lib-dynload',
 '/root/.local/lib/python3.10/site-packages',
 '/usr/local/lib/python3.10/site-packages']
Server time:	Sat, 04 Nov 2023 11:12:51 +0530
Traceback Switch to copy-and-paste view
/root/.local/lib/python3.10/site-packages/django/core/handlers/exception.py, line 55, in inner
        return inner
    else:
        @wraps(get_response)
        def inner(request):
            try:
                response = get_response(request) …
            except Exception as exc:
                response = response_for_exception(request, exc)
            return response
        return inner
Local vars
/root/.local/lib/python3.10/site-packages/django/core/handlers/base.py, line 197, in _get_response
        if response is None:
            wrapped_callback = self.make_view_atomic(callback)
            # If it is an asynchronous view, run it in a subthread.
            if iscoroutinefunction(wrapped_callback):
                wrapped_callback = async_to_sync(wrapped_callback)
            try:
                response = wrapped_callback(request, *callback_args, **callback_kwargs) …
            except Exception as e:
                response = self.process_exception_by_middleware(e, request)
                if response is None:
                    raise
        # Complain if the view returned None (a common error).
Local vars
/root/.local/lib/python3.10/site-packages/sentry_sdk/integrations/django/views.py, line 84, in sentry_wrapped_callback
            # this isn't necessary for async views since that runs on main
            if sentry_scope.profile is not None:
                sentry_scope.profile.update_active_thread_id()
            with hub.start_span(
                op=OP.VIEW_RENDER, description=request.resolver_match.view_name
            ):
                return callback(request, *args, **kwargs) …
    return sentry_wrapped_callback
Local vars
/usr/local/lib/python3.10/contextlib.py, line 79, in inner
        """
        return self
    def __call__(self, func):
        @wraps(func)
        def inner(*args, **kwds):
            with self._recreate_cm():
                return func(*args, **kwds) …
        return inner
class AsyncContextDecorator(object):
    "A base class or mixin that enables async context managers to work as decorators."
Local vars
/usr/local/lib/python3.10/contextlib.py, line 79, in inner
        """
        return self
    def __call__(self, func):
        @wraps(func)
        def inner(*args, **kwds):
            with self._recreate_cm():
                return func(*args, **kwds) …
        return inner
class AsyncContextDecorator(object):
    "A base class or mixin that enables async context managers to work as decorators."
Local vars
/root/.local/lib/python3.10/site-packages/django/views/decorators/csrf.py, line 56, in wrapper_view
def csrf_exempt(view_func):
    """Mark a view function as being exempt from the CSRF view protection."""
    # view_func.csrf_exempt = True would also work, but decorators are nicer
    # if they don't have side effects, so return a new function.
    @wraps(view_func)
    def wrapper_view(*args, **kwargs):
        return view_func(*args, **kwargs) …
    wrapper_view.csrf_exempt = True
    return wrapper_view
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/viewsets.py, line 125, in view
                setattr(self, method, handler)
            self.request = request
            self.args = args
            self.kwargs = kwargs
            # And continue as usual
            return self.dispatch(request, *args, **kwargs) …
        # take name and docstring from class
        update_wrapper(view, cls, updated=())
        # and possible attributes set by decorators
        # like csrf_exempt from dispatch
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/views.py, line 509, in dispatch
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed
            response = handler(request, *args, **kwargs)
        except Exception as exc:
            response = self.handle_exception(exc) …
        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response
    def options(self, request, *args, **kwargs):
        """
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/views.py, line 469, in handle_exception
        exception_handler = self.get_exception_handler()
        context = self.get_exception_handler_context()
        response = exception_handler(exc, context)
        if response is None:
            self.raise_uncaught_exception(exc) …
        response.exception = True
        return response
    def raise_uncaught_exception(self, exc):
        if settings.DEBUG:
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/views.py, line 480, in raise_uncaught_exception
    def raise_uncaught_exception(self, exc):
        if settings.DEBUG:
            request = self.request
            renderer_format = getattr(request.accepted_renderer, 'format')
            use_plaintext_traceback = renderer_format not in ('html', 'api', 'admin')
            request.force_plaintext_errors(use_plaintext_traceback)
        raise exc …
    # Note: Views are made CSRF exempt from within `as_view` as to prevent
    # accidental removal of this exemption in cases where `dispatch` needs to
    # be overridden.
    def dispatch(self, request, *args, **kwargs):
        """
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/views.py, line 506, in dispatch
            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed
            response = handler(request, *args, **kwargs) …
        except Exception as exc:
            response = self.handle_exception(exc)
        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/mixins.py, line 43, in list
    """
    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())
        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data) …
        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)
class RetrieveModelMixin:
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/serializers.py, line 768, in data
        return representation.list_repr(self, indent=1)
    # Include a backlink to the serializer class on return objects.
    # Allows renderers such as HTMLFormRenderer to get the full field info.
    @property
    def data(self):
        ret = super().data …
        return ReturnList(ret, serializer=self)
    @property
    def errors(self):
        ret = super().errors
        if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null':
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/serializers.py, line 253, in data
                'You should either call `.is_valid()` first, '
                'or access `.initial_data` instead.'
            )
            raise AssertionError(msg)
        if not hasattr(self, '_data'):
            if self.instance is not None and not getattr(self, '_errors', None):
                self._data = self.to_representation(self.instance) …
            elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):
                self._data = self.to_representation(self.validated_data)
            else:
                self._data = self.get_initial()
        return self._data
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/serializers.py, line 686, in to_representation
        """
        List of object instances -> List of dicts of primitive datatypes.
        """
        # Dealing with nested relationships, data can be a Manager,
        # so, first get a queryset from the Manager if needed
        iterable = data.all() if isinstance(data, models.Manager) else data
        return [ …
            self.child.to_representation(item) for item in iterable
        ]
    def validate(self, attrs):
        return attrs
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/serializers.py, line 687, in <listcomp>
        List of object instances -> List of dicts of primitive datatypes.
        """
        # Dealing with nested relationships, data can be a Manager,
        # so, first get a queryset from the Manager if needed
        iterable = data.all() if isinstance(data, models.Manager) else data
        return [
            self.child.to_representation(item) for item in iterable …
        ]
    def validate(self, attrs):
        return attrs
    def update(self, instance, validated_data):
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/serializers.py, line 522, in to_representation
            #
            # For related fields with `use_pk_only_optimization` we need to
            # resolve the pk value.
            check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
            if check_for_none is None:
                ret[field.field_name] = None
            else:
                ret[field.field_name] = field.to_representation(attribute) …
        return ret
    def validate(self, attrs):
        return attrs
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/serializers.py, line 509, in to_representation
        Object instance -> Dict of primitive datatypes.
        """
        ret = OrderedDict()
        fields = self._readable_fields
        for field in fields:
            try:
                attribute = field.get_attribute(instance) …
            except SkipField:
                continue
            # We skip `to_representation` for `None` values so that fields do
            # not have to explicitly deal with that case.
            #
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/fields.py, line 446, in get_attribute
    def get_attribute(self, instance):
        """
        Given the *outgoing* object instance, return the primitive value
        that should be used for this field.
        """
        try:
            return get_attribute(instance, self.source_attrs) …
        except BuiltinSignatureError as exc:
            msg = (
                'Field source for `{serializer}.{field}` maps to a built-in '
                'function type and is invalid. Define a property or method on '
                'the `{instance}` instance that wraps the call to the built-in '
                'function.'.format(
Local vars
/root/.local/lib/python3.10/site-packages/rest_framework/fields.py, line 96, in get_attribute
    Also accepts either attribute lookup on objects or dictionary lookups.
    """
    for attr in attrs:
        try:
            if isinstance(instance, Mapping):
                instance = instance[attr]
            else:
                instance = getattr(instance, attr) …
        except ObjectDoesNotExist:
            return None
        if is_simple_callable(instance):
            try:
                instance = instance()
            except (AttributeError, KeyError) as exc:
Local vars
/root/.local/lib/python3.10/site-packages/django_keycloak/models.py, line 69, in first_name
    @property
    def email(self):
        self._confirm_cache()
        return self._cached_user_info.get("email")
    @property
    def first_name(self):
        self._confirm_cache() …
        return self._cached_user_info.get("firstName")
    @property
    def last_name(self):
        self._confirm_cache()
        return self._cached_user_info.get("lastName")
Local vars
/root/.local/lib/python3.10/site-packages/django_keycloak/models.py, line 79, in _confirm_cache
    @property
    def last_name(self):
        self._confirm_cache()
        return self._cached_user_info.get("lastName")
    def _confirm_cache(self):
        if not self._cached_user_info:
            self._cached_user_info = lazy_keycloak_admin.get_user(self.id) …
class AbstractKeycloakUserAutoId(AbstractKeycloakUser):
    """
    This AbstractModel uses the default django AutoIncrement field as the PK,
    opposed to the AbstractKeycloakUser wich uses keycloak_id as the table PK.
Local vars
/root/.local/lib/python3.10/site-packages/keycloak/keycloak_admin.py, line 868, in get_user
        :param user_id: User id
        :type user_id: str
        :return: UserRepresentation
        """
        params_path = {"realm-name": self.connection.realm_name, "id": user_id}
        data_raw = self.connection.raw_get(urls_patterns.URL_ADMIN_USER.format(**params_path))
        return raise_error_from_response(data_raw, KeycloakGetError) …
    def get_user_groups(self, user_id, query=None, brief_representation=True):
        """Get user groups.
        Returns a list of groups of which the user is a member
Local vars
/root/.local/lib/python3.10/site-packages/keycloak/exceptions.py, line 192, in raise_error_from_response
    if isinstance(error, dict):
        error = error.get(response.status_code, KeycloakOperationError)
    else:
        if response.status_code == 401:
            error = KeycloakAuthenticationError
    raise error( …
        error_message=message, response_code=response.status_code, response_body=response.content
    )