Skip to content

Commit 8ea82b4

Browse files
committed
LDAP Group update for users with same gid/name in different OUs
1 parent 88587c5 commit 8ea82b4

2 files changed

Lines changed: 105 additions & 3 deletions

File tree

rcamp/accounts/admin.py

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from django.contrib import admin, messages
44
from django.contrib.auth import admin as auth_admin
55
from django import forms
6+
from django.http import Http404
67
from lib.fields import LdapCsvField
8+
from ldap.filter import escape_filter_chars
79
from accounts.models import (
810
User,
911
RcLdapUser,
@@ -16,6 +18,7 @@
1618
)
1719
from django.urls import reverse_lazy
1820
from django.shortcuts import render
21+
from django.contrib.admin.utils import quote, unquote
1922
#from .forms import ComanageSyncForm
2023
#from .models import ComanageUser
2124
#from comanage.lib import UserCO
@@ -213,10 +216,104 @@ class Meta:
213216

214217
@admin.register(RcLdapGroup)
215218
class RcLdapGroupAdmin(RcLdapModelAdmin):
216-
list_display = ['name','effective_cn','gid','members','organization',]
217-
search_fields = ['name']
219+
list_display = ['dn_display','name','effective_cn','gid','members','organization']
220+
# Make the DN column the clickable link
221+
list_display_links = ['dn_display']
222+
223+
# Optional: allow finding by DN substring (see §3)
224+
search_fields = ['name'] # 'dn' is not a DB field; handled in get_search_results
225+
218226
form = RcLdapGroupForm
219227

228+
# Column that shows the DN (human readable)
229+
def dn_display(self, obj):
230+
return obj.dn
231+
dn_display.short_description = "DN"
232+
233+
# Ensure changelist links use DN (unique)
234+
def url_for_result(self, result):
235+
return reverse("admin:accounts_rcldapgroup_change", args=(quote(result.dn),))
236+
237+
# Keep your DN-aware get_object (from earlier)
238+
def get_object(self, request, object_id, from_field=None):
239+
dn = unquote(object_id)
240+
if "=" in dn and "," in dn:
241+
try:
242+
return self.model.objects.get(dn=dn)
243+
except self.model.DoesNotExist:
244+
return None
245+
# Old name-based URL fallback (handle duplicates cleanly)
246+
qs = self.get_queryset(request).filter(name=object_id)
247+
count = qs.count()
248+
if count == 1:
249+
return qs.first()
250+
elif count == 0:
251+
return None
252+
else:
253+
raise Http404(
254+
f"Multiple groups named {object_id!r}. "
255+
"Open from the changelist (which uses DN) to pick the exact entry."
256+
)
257+
258+
def get_search_results(self, request, queryset, search_term):
259+
"""
260+
Restrict search to:
261+
- LDAP-side: name (cn) substring
262+
- Client-side: DN substring
263+
Then return a queryset filtered by OR of exact names of all matches.
264+
Avoid any pk/dn lookups to prevent 'Unsupported dn lookup: in'.
265+
"""
266+
qs, use_distinct = super().get_search_results(request, queryset, search_term)
267+
if not search_term:
268+
return qs, use_distinct
269+
270+
term = search_term.strip()
271+
term_escaped = escape_filter_chars(term)
272+
term_lower = term.lower()
273+
274+
# 1) LDAP-side: cn contains
275+
try:
276+
name_qs = queryset.filter(name__contains=term_escaped)
277+
names_from_name = set(name_qs.values_list('name', flat=True))
278+
except Exception:
279+
names_from_name = set()
280+
281+
# 2) Client-side: DN substring (iterate the queryset and test obj.dn)
282+
try:
283+
names_from_dn = {obj.name for obj in queryset if term_lower in obj.dn.lower()}
284+
except Exception:
285+
names_from_dn = set()
286+
287+
# 3) Merge names and filter by OR of exact name matches (no DN lookups)
288+
names = names_from_name | names_from_dn
289+
if not names:
290+
return queryset.none(), use_distinct
291+
292+
# Build a single OR expression: (cn=name1) OR (cn=name2) OR ...
293+
q = Q()
294+
for n in names:
295+
q |= Q(name=n)
296+
qs = queryset.filter(q)
297+
return qs, use_distinct
298+
299+
# Optional: redirect old name-only URLs to the DN URL when unique
300+
def change_view(self, request, object_id, form_url='', extra_context=None):
301+
dn_or_name = unquote(object_id)
302+
is_dn = ("=" in dn_or_name and "," in dn_or_name)
303+
304+
if not is_dn:
305+
qs = self.get_queryset(request).filter(name=dn_or_name)
306+
if qs.count() == 1:
307+
obj = qs.first()
308+
canonical = reverse("admin:accounts_rcldapgroup_change", args=(quote(obj.dn),))
309+
if request.GET:
310+
canonical = f"{canonical}?{request.META.get('QUERY_STRING','')}"
311+
return HttpResponseRedirect(canonical)
312+
313+
return super().change_view(request, object_id, form_url, extra_context)
314+
315+
316+
220317
# # Custom action to sync users from Comanage
221318
# def sync_users_from_comanage(modeladmin, request, queryset):
222319
# """

rcamp/accounts/models.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,11 @@ def create(self,*args,**kwargs):
443443
obj.save(force_insert=True,using=self.db,organization=org)
444444
return obj
445445

446+
def get_by_dn(self, dn: str):
447+
"""Fetch exactly one entry by its DN (authoritative, unique)."""
448+
return self.get(dn=dn)
449+
450+
446451
class RcLdapGroup(ldapdb.models.Model):
447452
class Meta:
448453
verbose_name = 'LDAP group'
@@ -469,7 +474,7 @@ def __init__(self,*args,**kwargs):
469474
# posixGroup attributes
470475
# gid = ldap_fields.IntegerField(db_column='gidNumber', unique=True)
471476
gid = ldap_fields.IntegerField(db_column='gidNumber',null=True,blank=True)
472-
name = ldap_fields.CharField(db_column='cn', max_length=200, primary_key=True)
477+
name = ldap_fields.CharField(db_column='cn', max_length=200)
473478
members = ldap_fields.ListField(db_column='memberUid',blank=True,null=True)
474479

475480
def __str__(self):

0 commit comments

Comments
 (0)