Skip to content

Commit 8ad2bcb

Browse files
committed
overhaul for comanage syncing
1 parent 3da070a commit 8ad2bcb

27 files changed

Lines changed: 1006 additions & 17 deletions

.vscode/launch.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "Python: Remote Attach",
6+
"type": "python",
7+
"request": "attach",
8+
"host": "localhost",
9+
"port": 5678,
10+
"pathMappings": [
11+
{
12+
"localRoot": "${workspaceFolder}/rcamp",
13+
"remoteRoot": "/opt/rcamp"
14+
}
15+
]
16+
}
17+
]
18+
}

Dockerfile

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ ARG GOSU_VERSION=1.16
99

1010
# Install dependencies
1111
RUN dnf -y install 'dnf-command(config-manager)' \
12-
&& dnf config-manager --set-enabled powertools \
13-
&& dnf -y install epel-release \
14-
&& dnf -y groupinstall "Development Tools" \
15-
&& dnf -y install xz dpkg which sssd pam_radius sqlite pam-devel openssl-devel python3-devel openldap-devel mysql-devel pcre-devel
12+
&& dnf config-manager --set-enabled powertools \
13+
&& dnf -y install epel-release \
14+
&& dnf -y groupinstall "Development Tools" \
15+
&& dnf -y install xz dpkg which sssd pam_radius sqlite pam-devel python3-devel openssl-devel openldap-devel mysql-devel pcre-devel
1616

1717
# Install gosu to drop user and chown shared volumes at runtime
1818
ADD ["https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-amd64", "/usr/bin/gosu"]
@@ -74,4 +74,5 @@ HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD curl
7474

7575
COPY ["docker-entrypoint.sh", "/usr/local/bin/"]
7676
ENTRYPOINT ["sh","/usr/local/bin/docker-entrypoint.sh"]
77+
#CMD python3 -m debugpy --listen 0.0.0.0:5678 --wait-for-client manage.py runserver 0.0.0.0:8000
7778
CMD ["/opt/rcamp_venv/bin/uwsgi", "/opt/uwsgi.ini"]

cmds

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
docker-compose run --rm --entrypoint "python3" rcamp-uwsgi manage.py migrate
2+
docker-compose run --rm --entrypoint "python3" rcamp-uwsgi manage.py createsuperuser

docker-compose.yml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
version: "3.6"
21

32
services:
43
rcamp-uwsgi:
54
build:
65
context: .
76
container_name: rcamp-uwsgi
87
command: ["python3", "manage.py", "runserver", "0.0.0.0:8000"]
8+
# command: ["python3", "-m", "debugpy", "--listen", "0.0.0.0:5678", "--wait-for-client", "manage.py", "runserver", "0.0.0.0:8000"]
99
env_file:
1010
- dev-environment.env
1111
environment:
@@ -19,9 +19,10 @@ services:
1919
- rcamp-logs:/opt/logs
2020
ports:
2121
- "80:8000"
22+
- "5678:5678" # Debugger port
2223
depends_on:
23-
- database
24-
- ldap
24+
database:
25+
condition: service_healthy
2526

2627
database:
2728
image: mysql:5.7
@@ -31,10 +32,11 @@ services:
3132
- MYSQL_DATABASE=rcamp1712
3233
volumes:
3334
- database:/var/lib/mysql
34-
35-
ldap:
36-
image: researchcomputing/rc-test-ldap
37-
container_name: ldap
35+
healthcheck:
36+
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -ppassword"]
37+
interval: 10s
38+
timeout: 5s
39+
retries: 5
3840

3941
volumes:
4042
static-content:

rcamp/accounts/admin.py

Lines changed: 278 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22

3-
from django.contrib import admin
3+
from django.contrib import admin, messages
44
from django.contrib.auth import admin as auth_admin
55
from django import forms
66
from lib.fields import LdapCsvField
@@ -10,10 +10,24 @@
1010
CuLdapUser,
1111
RcLdapGroup,
1212
IdTracker,
13+
ComanageUser,
14+
ComanageGroup,
1315
AccountRequest,
1416
Intent,
1517
ORGANIZATIONS
1618
)
19+
from django.urls import reverse_lazy
20+
from django.shortcuts import render
21+
from .forms import ComanageSyncForm
22+
from .models import ComanageUser
23+
from comanage.lib import UserCO
24+
from lib.utils import get_user_and_groups, get_comanage_users_by_org
25+
from django.urls import path
26+
from django.utils.html import format_html
27+
from django.urls import reverse
28+
from django.shortcuts import redirect
29+
from django.http import HttpResponseRedirect
30+
from django.db.models import Q
1731

1832

1933
@admin.register(User)
@@ -204,4 +218,267 @@ class RcLdapGroupAdmin(RcLdapModelAdmin):
204218
search_fields = ['name']
205219
form = RcLdapGroupForm
206220

221+
# Custom action to sync users from Comanage
222+
def sync_users_from_comanage(modeladmin, request, queryset):
223+
"""
224+
Sync the selected users from Comanage.
225+
"""
226+
if not queryset:
227+
users = ["kyre0001_amc"]
228+
for user in users:
229+
# try:
230+
# ComanageUser.sync_from_comanage(user.user_id) # Call the sync method on the user
231+
# modeladmin.message_user(request, f"User {user.name} synced successfully!")
232+
# except Exception as e:
233+
# modeladmin.message_user(request, f"Error syncing user {user.name}: {str(e)}", level='debug')
234+
try:
235+
ComanageUser.sync_from_comanage(user) # Call the sync method on the user
236+
modeladmin.message_user(request, f"User {user} synced successfully!")
237+
except Exception as e:
238+
modeladmin.message_user(request, f"Error syncing user {user}: {str(e)}", level='debug')
239+
240+
# Action to sync users in bulk
241+
def sync_users_from_comanage(modeladmin, request, queryset):
242+
"""
243+
Sync the selected users from Comanage.
244+
"""
245+
if queryset.count() == 0:
246+
modeladmin.message_user(request, "No users selected to sync.", level=messages.WARNING)
247+
return
248+
249+
for user in queryset:
250+
try:
251+
# Assuming ComanageUser.sync_from_comanage expects user ID
252+
ComanageUser.sync_from_comanage(user.user_id) # Sync logic here
253+
modeladmin.message_user(request, f"User {user.name} synced successfully!", level=messages.SUCCESS)
254+
except Exception as e:
255+
modeladmin.message_user(request, f"Error syncing user {user.name}: {str(e)}", level=messages.ERROR)
256+
257+
# ComanageUserAdmin with additional customization and sync functionality
258+
class ComanageUserAdmin(admin.ModelAdmin):
259+
list_display = ['user_id', 'name', 'email', 'co_person_id']
260+
search_fields = ['user_id', 'name', 'email', 'co_person_id']
261+
readonly_fields = ['user_id', 'name', 'email', 'co_person_id', 'group_names', 'created_at', 'modified']
262+
actions = [sync_users_from_comanage]
263+
264+
# Disable the "Add" button in the list view
265+
def has_add_permission(self, request):
266+
return False
267+
268+
# Disable the delete button for individual objects
269+
def has_delete_permission(self, request, obj=None):
270+
return False
271+
272+
def changelist_view(self, request, extra_context=None):
273+
"""
274+
Override the changelist view to trigger a function when the list of users is accessed.
275+
"""
276+
# Call your custom function here (e.g., sync users from Comanage)
277+
try:
278+
user = "kyre0001_amc" # Or you can filter users as needed
279+
ComanageUser.sync_from_comanage(user) # Call your sync function here
280+
self.message_user(request, "Users synced successfully!", level=messages.SUCCESS)
281+
except Exception as e:
282+
self.message_user(request, f"Error syncing users: {str(e)}", level=messages.ERROR)
283+
284+
# Proceed with the normal changelist view rendering
285+
return super().changelist_view(request, extra_context)
286+
287+
# Override the change_view method to remove the "Save" button and show custom sync button
288+
def change_view(self, request, object_id, form_url='', extra_context=None):
289+
extra_context = extra_context or {}
290+
extra_context['show_save_and_add_another'] = False
291+
extra_context['show_save_and_continue'] = False
292+
extra_context['show_save'] = False
293+
294+
return super().change_view(request, object_id, form_url, extra_context)
295+
296+
# class ComanageUserAdmin(admin.ModelAdmin):
297+
# list_display = ['user_id', 'name', 'email', 'co_person_id']
298+
# search_fields = ['user_id', 'name', 'email', 'co_person_id']
299+
# readonly_fields = ['user_id', 'name', 'email', 'co_person_id', 'group_names', 'created_at', 'modified',]
300+
# actions = [sync_users_from_comanage]
301+
302+
# # Disable the "Add" button in the list view
303+
# def has_add_permission(self, request):
304+
# return False
305+
306+
# # Disable the delete button for individual objects
307+
# def has_delete_permission(self, request, obj=None):
308+
# return False
309+
310+
# # Override the change_view method to remove the "Save" button
311+
# def change_view(self, request, object_id, form_url='', extra_context=None):
312+
# extra_context = extra_context or {}
313+
# extra_context['show_save_and_add_another'] = False
314+
# extra_context['show_save_and_continue'] = False
315+
# extra_context['show_save'] = False
316+
# return super().change_view(request, object_id, form_url, extra_context)
317+
318+
# Optionally, you can define a custom form (not shown here)
319+
# form = CustomUserForm # Uncomment and define your form if necessary
320+
321+
# class ComanageSyncAdmin(admin.ModelAdmin):
322+
# change_list_template = "comanage_sync_list.html" # Custom template for the changelist page
323+
# list_display = ('user_id', 'name', 'email', 'created_at', 'modified', 'actions')
324+
# search_fields = ['user_id', 'name', 'email']
325+
# actions = ['custom_action']
326+
327+
# def changelist_view(self, request, extra_context=None):
328+
# search_query = request.GET.get('q', '')
329+
330+
# # Filter user data if there is a search query
331+
# if search_query:
332+
# user_data = ComanageUser.objects.filter(
333+
# Q(user_id__icontains=search_query) | Q(email__icontains=search_query)
334+
# )
335+
# else:
336+
# # Fetch all user data if no search query is provided
337+
# user_data = ComanageUser.objects.all()
338+
339+
# # Add user data to the context for rendering
340+
# extra_context = extra_context or {}
341+
# extra_context['user_data'] = user_data
342+
# extra_context['search_query'] = search_query # Optional: to display the query in the search bar
343+
344+
# # Render the custom changelist view with the extra context
345+
# return super().changelist_view(request, extra_context=extra_context)
346+
347+
# # Define custom URL for detailed group view
348+
# def get_urls(self):
349+
# # Get default admin URLs
350+
# urls = super().get_urls()
351+
352+
# # Add custom URL for sync users and user detail view
353+
# custom_urls = [
354+
# path('sync-users/', self.admin_site.admin_view(self.sync_multiple_users), name='comanage-sync-users'),
355+
# path('comanage-sync/<str:user_id>/view-groups/', self.admin_site.admin_view(self.view_groups), name='comanage-sync-view-groups'),
356+
# path('comanage-sync/<str:user_id>/', self.admin_site.admin_view(self.comanage_sync_view), name='comanage-sync-user-detail'),
357+
# ]
358+
# return custom_urls + urls
359+
360+
# # View to show detailed user info and groups
361+
# def comanage_sync_view(self, request, user_id):
362+
# # Fetch user and group data based on user_id
363+
# user_data, groups = get_user_and_groups(user_id) # You need to implement this function
364+
365+
# # Render the user detail page with user data and groups
366+
# return render(request, 'comanage_sync_detail.html', {
367+
# 'user_data': user_data,
368+
# 'groups': groups,
369+
# })
370+
371+
# # Function to handle syncing multiple users
372+
# def sync_multiple_users(self, request):
373+
# # Define your list of user_ids to sync
374+
375+
# user_data = get_comanage_users_by_org()
376+
377+
# # Sync users from Comanage
378+
# for user in user_data:
379+
# ComanageUser.sync_from_comanage(user.cuhpcuid) # Sync method for updating users
380+
381+
# # Redirect back to the changelist view after sync is complete
382+
# self.message_user(request, "Users have been synced successfully!")
383+
# url = reverse('admin:accounts_comanageuser_changelist')
384+
385+
# return redirect(url) # Update this URL if necessary
386+
387+
# # Add actions to your admin class to display a custom button
388+
# def changelist_actions(self, request):
389+
# # Custom button that will trigger the sync function
390+
# return format_html('<a class="button" href="{}">Sync Users</a>',
391+
# self.admin_site.urls['comanage-sync-users']
392+
# )
393+
394+
# def custom_action(self, obj):
395+
# # Add "View Groups" link for each user
396+
# return format_html('<a href="{}" class="button">View Groups</a>',
397+
# reverse('admin:comanage-sync-user-detail', args=[obj.user_id])
398+
# )
399+
400+
# def view_groups(self, request, user_id):
401+
# try:
402+
# # Fetch the user based on user_id
403+
# user = ComanageUser.objects.get(user_id=user_id)
404+
405+
# # Fetch the user's groups, assuming you have a `groups` field or relation in your model
406+
# user_groups = user.groups.all()
407+
408+
# # Get members of each group (assuming there's a Many-to-Many or ForeignKey relationship with users)
409+
# group_members = {}
410+
# for group in user_groups:
411+
# group_members[group.name] = group.members.all() # Adjust based on your Group model
412+
413+
# return render(request, 'view_groups.html', {
414+
# 'user': user,
415+
# 'user_groups': user_groups,
416+
# 'group_members': group_members,
417+
# })
418+
419+
# except ComanageUser.DoesNotExist:
420+
# raise Http404("User not found")
421+
422+
class ComanageGroupForm(forms.ModelForm):
423+
def __init__(self, *args, **kwargs):
424+
super(ComanageGroupForm, self).__init__(*args, **kwargs)
425+
instance = getattr(self, 'instance', None)
426+
group_members = None
427+
428+
if instance and instance.pk:
429+
# If the group instance exists, filter out the members already in the group
430+
group_members = instance.members.all()
431+
available_users = ComanageUser.objects.all()
432+
else:
433+
# If this is a new group, show all users
434+
available_users = ComanageUser.objects.all()
435+
436+
# Prepare choices for the 'members' field (users not already in the group)
437+
user_choices = [
438+
(user.id, f'{user.name} ({user.user_id})') for user in available_users
439+
]
440+
441+
# Set pre-selected members based on the group instance
442+
self.fields['members'].initial = group_members
443+
444+
# Set the required fields
445+
self.fields['gid'].required = False
446+
self.fields['members'].required = False
447+
448+
# Apply the filtered multi-select widget
449+
self.fields['members'].widget = admin.widgets.FilteredSelectMultiple(
450+
verbose_name='Members',
451+
is_stacked=False
452+
)
453+
454+
# Assign the available choices dynamically
455+
self.fields['members'].choices = user_choices
456+
457+
class Meta:
458+
model = ComanageGroup
459+
fields = ['name', 'group_id', 'gid', 'created_at', 'modified', 'members']
460+
461+
class ComanageGroupAdmin(admin.ModelAdmin):
462+
list_display = ['name', 'gid', 'get_members']
463+
search_fields = ['name', 'group_id', 'gid']
464+
readonly_fields = ['group_id', 'created_at', 'modified']
465+
form = ComanageGroupForm
466+
467+
# Custom method to display group members
468+
def get_members(self, obj):
469+
"""
470+
Custom method to display all members of the group in the admin list view.
471+
"""
472+
return ", ".join([user.name for user in obj.members.all()])
473+
474+
get_members.short_description = 'Group Members'
475+
476+
def get_queryset(self, request):
477+
queryset = super().get_queryset(request)
478+
# Optionally, modify the queryset if needed
479+
return queryset
480+
481+
admin.site.register(ComanageGroup, ComanageGroupAdmin)
482+
admin.site.register(ComanageUser, ComanageUserAdmin)
483+
207484
admin.site.register(IdTracker)

rcamp/accounts/forms.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
REQUEST_ROLES
1313
)
1414

15+
class ComanageSyncForm(forms.Form):
16+
user_id = forms.CharField(max_length=255)
17+
1518
class AccountRequestVerifyForm(forms.Form):
1619
"""
1720
An abstract form for verifying user credentials against a configured authority

0 commit comments

Comments
 (0)