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
)