diff options
author | Justin Choquette <jchoquette@iol.unh.edu> | 2023-08-07 14:10:19 -0400 |
---|---|---|
committer | Justin Choquette <jchoquette@iol.unh.edu> | 2023-08-07 14:16:04 -0400 |
commit | a6168306c08e8d5b207b9acc48869180d194ff01 (patch) | |
tree | 51ffcafac4ae0b5fd4da363d9bf839e8ad3fc286 | |
parent | a09db9f287a02873c0226759f8ea444bb304cd59 (diff) |
User subsystem
Change-Id: Ibef4ede9b2d6a3ea465f79a9b5cbcc821afbccae
Signed-off-by: Justin Choquette <jchoquette@iol.unh.edu>
24 files changed, 721 insertions, 195 deletions
diff --git a/src/account/forms.py b/src/account/forms.py index 28cb27d..8bacc24 100644 --- a/src/account/forms.py +++ b/src/account/forms.py @@ -15,15 +15,11 @@ from django.utils.translation import gettext_lazy as _ from account.models import UserProfile -class AccountSettingsForm(forms.ModelForm): +class AccountPreferencesForm(forms.ModelForm): class Meta: model = UserProfile - fields = ['company', 'email_addr', 'public_user', 'ssh_public_key', 'pgp_public_key', 'timezone'] + fields = ['timezone', 'public_user'] labels = { - 'email_addr': _('Email Address'), - 'ssh_public_key': _('SSH Public Key'), - 'pgp_public_key': _('PGP Public Key'), - 'public_user': _('Public User') } - - timezone = forms.ChoiceField(choices=[(x, x) for x in pytz.common_timezones], initial='UTC') + timezone = forms.ChoiceField(widget=forms.Select(attrs={'style': 'width: 200px;', 'class': 'form-control'}) ,choices=[(x, x) for x in pytz.common_timezones], initial='UTC') + public_user = forms.BooleanField(required=False, widget=forms.CheckboxInput(attrs={}))
\ No newline at end of file diff --git a/src/account/migrations/0011_userprofile_ipa_username.py b/src/account/migrations/0011_userprofile_ipa_username.py new file mode 100644 index 0000000..25cf6fb --- /dev/null +++ b/src/account/migrations/0011_userprofile_ipa_username.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2023-07-24 20:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0010_auto_20230608_1913'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='ipa_username', + field=models.CharField(max_length=100, null=True), + ), + ] diff --git a/src/account/migrations/0012_auto_20230725_1749.py b/src/account/migrations/0012_auto_20230725_1749.py new file mode 100644 index 0000000..3f4d142 --- /dev/null +++ b/src/account/migrations/0012_auto_20230725_1749.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2023-07-25 17:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0011_userprofile_ipa_username'), + ] + + operations = [ + migrations.AlterField( + model_name='userprofile', + name='ipa_username', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/src/account/migrations/0013_auto_20230727_1903.py b/src/account/migrations/0013_auto_20230727_1903.py new file mode 100644 index 0000000..9e1d222 --- /dev/null +++ b/src/account/migrations/0013_auto_20230727_1903.py @@ -0,0 +1,29 @@ +# Generated by Django 2.2 on 2023-07-27 19:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0012_auto_20230725_1749'), + ] + + operations = [ + migrations.RemoveField( + model_name='userprofile', + name='company', + ), + migrations.RemoveField( + model_name='userprofile', + name='jira_url', + ), + migrations.RemoveField( + model_name='userprofile', + name='pgp_public_key', + ), + migrations.RemoveField( + model_name='userprofile', + name='ssh_public_key', + ), + ] diff --git a/src/account/models.py b/src/account/models.py index f1deca7..bb1cad5 100644 --- a/src/account/models.py +++ b/src/account/models.py @@ -39,20 +39,16 @@ class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) timezone = models.CharField(max_length=100, blank=False, default='UTC') - ssh_public_key = models.FileField(upload_to=upload_to, null=True, blank=True) - pgp_public_key = models.FileField(upload_to=upload_to, null=True, blank=True) email_addr = models.CharField(max_length=300, blank=False, default='email@mail.com') - company = models.CharField(max_length=200, blank=False) oauth_token = models.CharField(max_length=1024, blank=False) oauth_secret = models.CharField(max_length=1024, blank=False) - jira_url = models.CharField(max_length=100, null=True, blank=True, default='') - full_name = models.CharField(max_length=100, null=True, blank=True, default='') booking_privledge = models.BooleanField(default=False) public_user = models.BooleanField(default=False) + ipa_username = models.CharField(max_length=100, null=True, blank=True) class Meta: db_table = 'user_profile' diff --git a/src/account/urls.py b/src/account/urls.py index 23ce122..35ef43b 100644 --- a/src/account/urls.py +++ b/src/account/urls.py @@ -29,24 +29,22 @@ from django.conf.urls import url from django.urls import path from account.views import ( - AccountSettingsView, OIDCLoginView, LogoutView, - UserListView, account_resource_view, account_booking_view, account_detail_view, template_delete_view, booking_cancel_view, + account_settings_view ) app_name = 'account' urlpatterns = [ - url(r'^settings/', AccountSettingsView.as_view(), name='settings'), + url(r'^settings/', account_settings_view, name='settings'), url(r'^login/$', OIDCLoginView.as_view(), name='login'), url(r'^logout/$', LogoutView.as_view(), name='logout'), - url(r'^users/$', UserListView.as_view(), name='users'), url(r'^my/resources/$', account_resource_view, name='my-resources'), path('my/resources/delete/<int:resource_id>', template_delete_view), url(r'^my/bookings/$', account_booking_view, name='my-bookings'), diff --git a/src/account/views.py b/src/account/views.py index 2d280cd..a975a2e 100644 --- a/src/account/views.py +++ b/src/account/views.py @@ -20,38 +20,55 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.urls import reverse from django.http import HttpResponse -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, redirect, render from django.utils.decorators import method_decorator from django.views.generic import RedirectView, TemplateView, UpdateView from django.shortcuts import render +from api.utils import ipa_set_ssh, ipa_query_user, ipa_set_company +from dashboard.forms import SetCompanyForm, SetSSHForm from rest_framework.authtoken.models import Token from mozilla_django_oidc.auth import OIDCAuthenticationBackend -from account.forms import AccountSettingsForm +from account.forms import AccountPreferencesForm from account.models import UserProfile from booking.models import Booking from api.views import delete_template, liblaas_templates -@method_decorator(login_required, name='dispatch') -class AccountSettingsView(UpdateView): - model = UserProfile - form_class = AccountSettingsForm - template_name_suffix = '_update_form' - - def get_success_url(self): - messages.add_message(self.request, messages.INFO, - 'Settings saved') - return '/' +from workflow.views import login + +def account_settings_view(request): + if request.method == "GET": + if not request.user.is_authenticated: + return login(request) + profile = UserProfile.objects.get(user=request.user) + if (not profile or profile.ipa_username == "" or profile.ipa_username == None): + return redirect("dashboard:index") + ipa_user = ipa_query_user(profile.ipa_username) + template = "account/settings.html" + context = { + "preference_form": AccountPreferencesForm(instance=profile), + "company_form": SetCompanyForm(initial={'company': ipa_user['ou']}), + "existing_keys": ipa_user['ipasshpubkey'] if 'ipasshpubkey' in ipa_user else [] + } + return render(request, template, context) + + if request.method == 'POST': + data = request.POST - def get_object(self, queryset=None): - return self.request.user.userprofile + print("data is", data) + # User profile + profile = UserProfile.objects.get(user=request.user) + profile.public_user = "public_user" in data + profile.timezone = data["timezone"] + profile.save() - def get_context_data(self, **kwargs): - token, created = Token.objects.get_or_create(user=self.request.user) - context = super(AccountSettingsView, self).get_context_data(**kwargs) - context.update({'title': "Settings", 'token': token}) - return context + # IPA + ipa_set_company(profile, data['company']) + ipa_set_ssh(profile, data['ssh_key_list'].split(",")) + return redirect("account:settings") + + return HttpResponse(status=405) class MyOIDCAB(OIDCAuthenticationBackend): def filter_users_by_claims(self, claims): @@ -106,17 +123,6 @@ class LogoutView(LoginRequiredMixin, RedirectView): return '/' -@method_decorator(login_required, name='dispatch') -class UserListView(TemplateView): - template_name = "account/user_list.html" - - def get_context_data(self, **kwargs): - users = UserProfile.objects.filter(public_user=True).select_related('user') - context = super(UserListView, self).get_context_data(**kwargs) - context.update({'title': "Dashboard Users", 'users': users}) - return context - - def account_detail_view(request): template = "account/details.html" return render(request, template) @@ -134,10 +140,12 @@ def account_resource_view(request): template = "account/resource_list.html" if request.method == "GET": - + profile = UserProfile.objects.get(user=request.user) + if (not profile or profile.ipa_username == "" or profile.ipa_username == None): + return redirect("dashboard:index") r = liblaas_templates(request) usable_templates = r.json() - user_templates = [ t for t in usable_templates if t["owner"] == str(request.user)] + user_templates = [ t for t in usable_templates if t["owner"] == profile.ipa_username] context = { "templates": user_templates, "title": "My Resources" @@ -153,6 +161,9 @@ def account_resource_view(request): def account_booking_view(request): if not request.user.is_authenticated: return render(request, "dashboard/login.html", {'title': 'Authentication Required'}) + profile = UserProfile.objects.get(user=request.user) + if (not profile or profile.ipa_username == "" or profile.ipa_username == None): + return redirect("dashboard:index") template = "account/booking_list.html" bookings = list(Booking.objects.filter(owner=request.user, end__gt=timezone.now()).order_by("-start")) my_old_bookings = Booking.objects.filter(owner=request.user, end__lt=timezone.now()).order_by("-start") diff --git a/src/api/urls.py b/src/api/urls.py index b009aeb..755b61f 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -42,6 +42,11 @@ from api.views import ( list_labs, all_users, booking_details, + ipa_create_account, + ipa_confirm_account, + ipa_set_company_from_workflow, + ipa_add_ssh_from_workflow, + ipa_conflict_account ) urlpatterns = [ @@ -60,5 +65,10 @@ urlpatterns = [ path('users', all_users), path('labs', list_labs), + path('ipa/create', ipa_create_account), + path('ipa/confirm', ipa_confirm_account), + path('ipa/conflict', ipa_conflict_account), + path('ipa/workflow-company', ipa_set_company_from_workflow), + path('ipa/workflow-ssh', ipa_add_ssh_from_workflow), url(r'^token$', GenerateTokenView.as_view(), name='generate_token'), ] diff --git a/src/api/utils.py b/src/api/utils.py new file mode 100644 index 0000000..c32205e --- /dev/null +++ b/src/api/utils.py @@ -0,0 +1,122 @@ +# These functions are called from views and perform the actual request to LibLaaS + +import json +from django.http.response import JsonResponse, HttpResponse +import requests +import os + +from dashboard.forms import * +liblaas_base_url = os.environ.get("LIBLAAS_BASE_URL") + +# IPA Stuff +def ipa_query_user(ipa_username): + url = liblaas_base_url + "user/" + ipa_username + print("Getting ipa user for", ipa_username, url) + try: + response = requests.get(url) + data = response.json() + print("ipa user is", data) + return data # Expects a dict + except Exception as e: + print(e) + return None + +# Queries for an IPA user using dashboard username +# Returns a result +def get_ipa_migration_form(user, profile): + # ipa_user = ipa_query_user(str(dashboard_user)) + # if (ipa_user and ipa_user.mail is ) + # pass + dashboard_username = str(user) + dashboard_email = profile.email_addr + first_name = user.first_name + last_name = user.last_name + + ipa_user = ipa_query_user(dashboard_username) + print("Attempting auto migration with", dashboard_username, dashboard_email, ipa_user) + if (ipa_user): + if (dashboard_email == ipa_user["mail"]): + # User is found and email match + print("User is found and email match") + return { + "form": ReadOnlyIPAAccountForm(initial={'ipa_username': ipa_user['uid'],'first_name': ipa_user["givenname"], 'last_name': ipa_user["sn"], 'email': ipa_user["mail"], 'company': ipa_user["ou"]}), + "message": "We have located the following IPA account matching your username and email. Please confirm to link your account. You may change these details at any time.", + "action": "api/ipa/confirm", + "button": "Link" + } + + else: + # User is found and emails don't match + print("User is found and emails don't match") + return { + "form": ConflictIPAAcountForm(initial={'first_name': first_name, 'last_name': last_name, 'email': dashboard_email}), + "message": "Our records indicate that you do not currently have an account in our IPA system, or your emails do not match. Please enter the following details to enroll your account.", + "action": "/", + "button": "Submit" + } + else: + # User is not found + print("User is not found") + return { + "form": NewIPAAccountForm(initial={'first_name': first_name, 'last_name': last_name, 'email': dashboard_email}), + "message": "Our records indicate that you do not currently have an account in our IPA system, or your usernames do not match. Please enter the following details to enroll your account.", + "action": "api/ipa/create", + "button": "Submit" + } + +# Take a list of strings, sends it to liblaas, replacing the IPA keys with the new keys +def ipa_set_ssh(user_profile, ssh_key_list): + url = liblaas_base_url + "user/" + user_profile.ipa_username + "/ssh" + print(ssh_key_list) + print("Setting SSH keys with URL", url) + try: + requests.post(url, data=json.dumps(ssh_key_list), headers={'Content-Type': 'application/json'}) + return HttpResponse(status=200) + except Exception as e: + print(e) + return HttpResponse(status=500) + +def ipa_set_company(user_profile, company_name): + url = liblaas_base_url + "user/" + user_profile.ipa_username + "/company" + print("Setting company with URL", url) + try: + requests.post(url, data=json.dumps(company_name), headers={'Content-Type': 'application/json'}) + return HttpResponse(status=200) + except Exception as e: + print(e) + return HttpResponse(status=500) + +def get_booking_prereqs_validator(user_profile): + ipa_user = None + if (user_profile.ipa_username != None and user_profile.ipa_username != ""): + ipa_user = ipa_query_user(user_profile.ipa_username) + + if ipa_user == None: + print("No user") + return { + "form": None, + "exists": "false", + "action": "no user" + } + + if (not "ou" in ipa_user) or (ipa_user["ou"] == ""): + print("Missing company") + return { + "form": SetCompanyForm(), + "exists": "true", + "action": "/api/ipa/workflow-company" + } + + if (not "ipasshpubkey" in ipa_user) or (ipa_user["ipasshpubkey"] == []): + print("Missing SSH key") + return { + "form": SetSSHForm(), + "exists": "true", + "action": "/api/ipa/workflow-ssh" + } + + return { + "form": None, + "exists": "false", + "action": "" + }
\ No newline at end of file diff --git a/src/api/views.py b/src/api/views.py index ea36a6d..98dd3dc 100644 --- a/src/api/views.py +++ b/src/api/views.py @@ -23,7 +23,7 @@ from django.views import View from django.http import HttpResponseNotFound from django.http.response import JsonResponse, HttpResponse import requests -from rest_framework import viewsets +from api.utils import ipa_query_user, ipa_set_ssh, ipa_set_company from rest_framework.authtoken.models import Token from django.views.decorators.csrf import csrf_exempt from django.core.exceptions import ObjectDoesNotExist @@ -51,7 +51,7 @@ Most functions let you GET or POST to the same endpoint, and the correct thing will happen """ - +liblaas_base_url = os.environ.get('LIBLAAS_BASE_URL') @method_decorator(login_required, name='dispatch') class GenerateTokenView(View): def get(self, request, *args, **kwargs): @@ -249,20 +249,31 @@ def make_booking(request): data = json.loads(request.body) print("incoming data is ", data) - allowed_users = list(data["allowed_users"]) - allowed_users.append(str(request.user)) + # todo - test this + ipa_users = list(UserProfile.objects.get(user=request.user).ipa_username) # add owner's ipa username to list of allowed users to be sent to liblaas + + for user in list(data["allowed_users"]): + collab_profile = UserProfile.objects.get(user=User.objects.get(username=user)) + if (collab_profile.ipa_username == "" or collab_profile.ipa_username == None): + return JsonResponse( + data={}, + status=406, # Not good practice but a quick solution until blob validation is fully supported within django instead of the frontend + safe=False + ) + else: + ipa_users.append(collab_profile.ipa_username) bookingBlob = { "template_id": data["template_id"], - "allowed_users": allowed_users, + "allowed_users": ipa_users, "global_cifile": data["global_cifile"], "metadata": { "booking_id": None, # fill in after creating django object - "owner": str(request.user), + "owner": UserProfile.objects.get(user=request.user).ipa_username, "lab": "UNH_IOL", "purpose": data["metadata"]["purpose"], "project": data["metadata"]["project"], - "length": data["metadata"]["length"] + "length": int(data["metadata"]["length"]) } } @@ -279,9 +290,8 @@ def make_booking(request): print("successfully created booking object with id ", booking.id) # Now add collabs - for c in bookingBlob["allowed_users"]: - if c != bookingBlob["metadata"]["owner"]: # Don't add self as a collab - booking.collaborators.add(User.objects.get(username=c)) + for c in list(data["allowed_users"]): + booking.collaborators.add(User.objects.get(username=c)) print("successfully added collabs") # Now create it in liblaas @@ -479,7 +489,7 @@ def liblaas_request(request) -> JsonResponse: liblaas_endpoint = post_data["endpoint"] payload = post_data["workflow_data"] # Fill in actual username - liblaas_endpoint = liblaas_endpoint.replace("[username]", str(request.user)) + liblaas_endpoint = liblaas_endpoint.replace("[username]", UserProfile.objects.get(user=request.user).ipa_username) liblaas_endpoint = liblaas_base_url + liblaas_endpoint print("processed endpoint is ", liblaas_endpoint) @@ -513,7 +523,7 @@ def liblaas_request(request) -> JsonResponse: ) def liblaas_templates(request): - liblaas_url = os.environ.get("LIBLAAS_BASE_URL") + "template/list/" + str(request.user) + liblaas_url = os.environ.get("LIBLAAS_BASE_URL") + "template/list/" + UserProfile.objects.get(user=request.user).ipa_username print("api call to " + liblaas_url) return requests.get(liblaas_url) @@ -553,4 +563,86 @@ def liblaas_end_booking(aggregateId): return response except: print("failed to end booking") - return HttpResponse(status=500)
\ No newline at end of file + return HttpResponse(status=500) + +def ipa_create_account(request): + # Called when no username was found + # Only allow if user does not have a linked ipa account + profile = UserProfile.objects.get(user=request.user) + if (profile.ipa_username): + return HttpResponse(status=401) + + post_data = request.POST + user = { + 'uid': str(request.user), + 'givenname': post_data['first_name'], + 'sn': post_data['last_name'], + 'cn': post_data['first_name'] + " " + post_data['last_name'], + 'mail': post_data['email'], + 'ou': post_data['company'], + 'random': True + } + + try: + response = requests.post(liblaas_base_url + "user/create", data=json.dumps(user), headers={'Content-Type': 'application/json'}) + profile.ipa_username = user['uid'] + print("Saving ipa username", profile.ipa_username) + profile.save() + return redirect("dashboard:index") + except Exception as e: + print(e) + return redirect("dashboard:index") + +def ipa_confirm_account(request): + # Called when username was found and email matches + profile = UserProfile.objects.get(user=request.user) + if (profile.ipa_username): + return HttpResponse(status=401) + + profile.ipa_username = str(request.user) + print("Saving ipa username", profile.ipa_username) + profile.save() + return redirect("dashboard:index") + +def ipa_conflict_account(request): + # Called when username was found but emails do not match + # Need to ask user to input alternate username + # To verify username is not taken, call query_username and accept if returns None + print("ipa conflict account") + profile = UserProfile.objects.get(user=request.user) + print("profile is", profile) + if (profile.ipa_username): + return HttpResponse(status=401) + + post_data = request.POST + user = { + 'uid': post_data['ipa_username'], + 'givenname': post_data['first_name'], + 'sn': post_data['last_name'], + 'cn': post_data['first_name'] + " " + post_data['last_name'], + 'mail': post_data['email'], + 'ou': post_data['company'], + 'random': True, + } + + try: + response = requests.post(liblaas_base_url + "user/create", data=json.dumps(user), headers={'Content-Type': 'application/json'}) + profile.ipa_username = user['uid'] + print("Saving ipa username", profile.ipa_username) + profile.save() + return redirect("dashboard:index") + except Exception as e: + print(e) + return redirect("dashboard:index") + +def ipa_set_company_from_workflow(request): + profile = UserProfile.objects.get(user=request.user) + ipa_set_company(profile, request.POST["company"]) + return redirect("workflow:book_a_pod") + +def ipa_add_ssh_from_workflow(request): + profile = UserProfile.objects.get(user=request.user) + key_as_list = [] + key_as_list.append(request.POST["ssh_public_key"]) + ipa_set_ssh(profile, key_as_list) + return redirect("workflow:book_a_pod") diff --git a/src/booking/migrations/0012_auto_20230802_1810.py b/src/booking/migrations/0012_auto_20230802_1810.py new file mode 100644 index 0000000..36095a1 --- /dev/null +++ b/src/booking/migrations/0012_auto_20230802_1810.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2 on 2023-08-02 18:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0011_booking_aggregateid'), + ] + + operations = [ + migrations.AlterField( + model_name='booking', + name='aggregateId', + field=models.CharField(blank=True, max_length=36), + ), + ] diff --git a/src/booking/models.py b/src/booking/models.py index 09244d3..eb72eb2 100644 --- a/src/booking/models.py +++ b/src/booking/models.py @@ -34,7 +34,7 @@ class Booking(models.Model): pdf = models.TextField(blank=True, default="") idf = models.TextField(blank=True, default="") # Associated LibLaaS aggregate - aggregateId = models.CharField(blank=True, max_length=36, validators=[RegexValidator(regex='^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$', message='aggregate_id must be a valid UUID', code='nomatch')]) + aggregateId = models.CharField(blank=True, max_length=36) complete = models.BooleanField(default=False) diff --git a/src/dashboard/forms.py b/src/dashboard/forms.py new file mode 100644 index 0000000..dd87ec6 --- /dev/null +++ b/src/dashboard/forms.py @@ -0,0 +1,46 @@ +from django import forms + +class NewIPAAccountForm(forms.Form): + + # First name + first_name = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'First Name', 'style': 'width: 300px;', 'class': 'form-control'})) + # Last name + last_name = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Last Name', 'style': 'width: 300px;', 'class': 'form-control'})) + # Company + company = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Company', 'style': 'width: 300px;', 'class': 'form-control'})) + # Email + email = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Email', 'style': 'width: 300px;', 'class': 'form-control', "readonly": True})) + +class ReadOnlyIPAAccountForm(forms.Form): + # IPA Username + ipa_username = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'IPA Username', 'style': 'width: 300px;', 'class': 'form-control', "readonly": True})) + # First name + first_name = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'First Name', 'style': 'width: 300px;', 'class': 'form-control', "readonly": True})) + # Last name + last_name = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'Last Name', 'style': 'width: 300px;', 'class': 'form-control', "readonly": True})) + # Company + company = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'Company', 'style': 'width: 300px;', 'class': 'form-control', "readonly": True})) + # Email + email = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'Email', 'style': 'width: 300px;', 'class': 'form-control', "readonly": True})) + +class ConflictIPAAcountForm(forms.Form): + # IPA Username + ipa_username = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'IPA Username', 'style': 'width: 300px;', 'class': 'form-control'})) + # First name + first_name = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'First Name', 'style': 'width: 300px;', 'class': 'form-control'})) + # Last name + last_name = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Last Name', 'style': 'width: 300px;', 'class': 'form-control'})) + # Company + company = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Company', 'style': 'width: 300px;', 'class': 'form-control'})) + # Email + email = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Email', 'style': 'width: 300px;', 'class': 'form-control', "readonly": True})) + +class SetCompanyForm(forms.Form): + # Company + company = forms.CharField(required=True, widget=forms.TextInput(attrs={'placeholder': 'Company', 'style': 'width: 300px;', 'class': 'form-control'})) + +class SetSSHForm(forms.Form): + # SSH key + ssh_public_key = forms.CharField(required=True, widget=forms.Textarea(attrs={'placeholder': 'SSH Public Key', 'style': 'width: 300px;', 'class': 'form-control'})) + + diff --git a/src/dashboard/views.py b/src/dashboard/views.py index 2942d59..f250a3c 100644 --- a/src/dashboard/views.py +++ b/src/dashboard/views.py @@ -16,8 +16,11 @@ from django.db.models import Q from datetime import datetime import pytz -from account.models import Lab +from account.models import Lab, UserProfile +from api.utils import get_ipa_migration_form, ipa_query_user +from api.views import ipa_conflict_account from booking.models import Booking +from dashboard.forms import * from laas_dashboard import settings @@ -72,25 +75,55 @@ def host_profile_detail_view(request): def landing_view(request): user = request.user + ipa_migrator = { + "exists": "false" # Jinja moment + } if not user.is_anonymous: bookings = Booking.objects.filter( Q(owner=user) | Q(collaborators=user), end__gte=datetime.now(pytz.utc) ) + profile = UserProfile.objects.get(user=user) + if (not profile.ipa_username): + ipa_migrator = get_ipa_migration_form(user, profile) + ipa_migrator["exists"] = "true" + else: bookings = None + print("IPA migrator is", ipa_migrator) LFID = True if settings.AUTH_SETTING == 'LFID' else False - return render( - request, - 'dashboard/landing.html', - { - 'title': "Welcome to the Lab as a Service Dashboard", - 'bookings': bookings, - 'LFID': LFID - } - ) + if request.method == "GET": + return render( + request, + 'dashboard/landing.html', + { + 'title': "Welcome to the Lab as a Service Dashboard", + 'bookings': bookings, + 'LFID': LFID, + 'ipa_migrator': ipa_migrator, + } + ) + + # Using this for the special case in the ipa_migrator + if request.method == 'POST': + existing_profile = ipa_query_user(request.POST['ipa_username']) + print("exists already?", existing_profile != None) + if (existing_profile != None): + return render( + request, + 'dashboard/landing.html', + { + 'title': "Welcome to the Lab as a Service Dashboard", + 'bookings': bookings, + 'LFID': LFID, + 'ipa_migrator': ipa_migrator, + 'error': "Username is already taken" + } + ) + else: + return ipa_conflict_account(request) class LandingView(TemplateView): template_name = "dashboard/landing.html" diff --git a/src/static/js/workflows/book-a-pod.js b/src/static/js/workflows/book-a-pod.js index d573342..3f83849 100644 --- a/src/static/js/workflows/book-a-pod.js +++ b/src/static/js/workflows/book-a-pod.js @@ -71,7 +71,7 @@ const steps = { } isValidCIFile(ci_file) { - // todo + // todo return true; } @@ -232,34 +232,37 @@ const steps = { return[passed, message, section]; } - onclickCancel() { - if (confirm("Are you sure you wish to discard this booking?")) { - location.reload(); - } - } + // onclickCancel() { + // if (confirm("Are you sure you wish to discard this booking?")) { + // location.reload(); + // } + // } /** Async / await is more infectious than I thought, so all functions that rely on an API call will need to be async */ async onclickConfirm() { const complete = this.isCompleteBookingInfo(); if (!complete[0]) { - alert(complete[1]); + showError(complete[1]); this.step = complete[2] document.getElementById(this.sections[complete[2]]).scrollIntoView({behavior: 'smooth'}); return } - if (confirm("Are you sure you would like to create this booking?")) { - const response = await LibLaaSAPI.makeBooking(this.bookingBlob); - if (response.bookingId) { - alert("The booking has been successfully created.") - window.location.href = "../../"; + + const response = await LibLaaSAPI.makeBooking(this.bookingBlob); + if (response.bookingId) { + showError("The booking has been successfully created.") + window.location.href = "../../"; + } else { + if (response.status == 406) { + showError("One or more collaborators is missing SSH keys or has not configured their IPA account.") } else { - alert("The booking could not be created at this time.") + showError("The booking could not be created at this time.") } } - } - - + // if (confirm("Are you sure you would like to create this booking?")) { + // } + } } diff --git a/src/static/js/workflows/design-a-pod.js b/src/static/js/workflows/design-a-pod.js index 7632537..58f8b85 100644 --- a/src/static/js/workflows/design-a-pod.js +++ b/src/static/js/workflows/design-a-pod.js @@ -110,13 +110,13 @@ class DesignWorkflow extends Workflow { this.step = steps.ADD_RESOURCES; if (this.templateBlob.lab_name == null) { - alert("Please select a lab before adding resources."); + showError("Please select a lab before adding resources."); this.goTo(steps.SELECT_LAB); return; } if (this.templateBlob.host_list.length >= 8) { - alert("You may not add more than 8 hosts to a single pod.") + showError("You may not add more than 8 hosts to a single pod.") return; } @@ -282,7 +282,7 @@ class DesignWorkflow extends Workflow { } } - alert("didnt remove"); + showError("didnt remove"); } @@ -297,13 +297,13 @@ class DesignWorkflow extends Workflow { this.step = steps.ADD_NETWORKS; if (this.templateBlob.lab_name == null) { - alert("Please select a lab before adding networks."); + showError("Please select a lab before adding networks."); this.goTo(steps.SELECT_LAB); return; } if (document.querySelector('#new_network_card') != null) { - alert("Please finish adding the current network before adding a new one."); + showError("Please finish adding the current network before adding a new one."); return; } @@ -377,7 +377,7 @@ class DesignWorkflow extends Workflow { } } - alert("didnt remove"); + showError("didnt remove"); } /** Rebuilds the list without the chosen template */ @@ -407,7 +407,7 @@ class DesignWorkflow extends Workflow { const host = this.templateBlob.findHost(hostname); if (!host) { - alert("host not found error"); + showError("host not found error"); } this.connectionTemp = new ConnectionTemp(host, this.templateBlob.networks, this.labFlavors.get(host.flavor).interfaces); @@ -440,11 +440,11 @@ class DesignWorkflow extends Workflow { setPodDetailEventListeners() { const pod_name_input = document.getElementById("pod-name-input"); const pod_desc_input = document.getElementById("pod-desc-input"); - const pod_public_input = document.getElementById("pod-public-input"); + // const pod_public_input = document.getElementById("pod-public-input"); pod_name_input.value = ""; pod_desc_input.value = ""; - pod_public_input.checked = false; + // pod_public_input.checked = false; pod_name_input.addEventListener('focusout', (event)=> { workflow.onFocusOutPodNameInput(pod_name_input); @@ -466,10 +466,10 @@ class DesignWorkflow extends Workflow { GUI.hidePodDetailsError(); }); - pod_public_input.addEventListener('focusout', (event)=> { - this.step = steps.POD_DETAILS; - workflow.onFocusOutPodPublicInput(pod_public_input); - }); + // pod_public_input.addEventListener('focusout', (event)=> { + // this.step = steps.POD_DETAILS; + // workflow.onFocusOutPodPublicInput(pod_public_input); + // }); } onFocusOutPodNameInput(element) { @@ -524,13 +524,13 @@ class DesignWorkflow extends Workflow { return [result, message] } - async onclickDiscardTemplate() { - this.step = steps.POD_SUMMARY; - if(confirm('Are you sure you wish to delete this Pod?')) { - await LibLaaSAPI.deleteTemplate(this.templateBlob); - location.reload(); - } - } + // async onclickDiscardTemplate() { + // this.step = steps.POD_SUMMARY; + // if(confirm('Are you sure you wish to delete this Pod?')) { + // await LibLaaSAPI.deleteTemplate(this.templateBlob); + // location.reload(); + // } + // } simpleStepValidation() { let passed = true; @@ -561,20 +561,21 @@ class DesignWorkflow extends Workflow { this.step = steps.POD_SUMMARY; const simpleValidation = this.simpleStepValidation(); if (!simpleValidation[0]) { - alert(simpleValidation[1]) + showError(simpleValidation[1]) this.goTo(simpleValidation[2]); return; } // todo - make sure each host has at least one connection on any network. - if (confirm("Are you sure you wish to create this pod?")) { - let success = await LibLaaSAPI.makeTemplate(this.templateBlob); - if (success) { - window.location.href = "../../accounts/my/resources/"; - } else { - alert("Could not create template.") - } + // if (confirm("Are you sure you wish to create this pod?")) { + + // } + let success = await LibLaaSAPI.makeTemplate(this.templateBlob); + if (success) { + window.location.href = "../../accounts/my/resources/"; + } else { + showError("Could not create template.") } } } @@ -1182,5 +1183,5 @@ class ConnectionTemp { } function todo() { - alert('todo'); + showError('todo'); }
\ No newline at end of file diff --git a/src/static/js/workflows/workflow.js b/src/static/js/workflows/workflow.js index 745a706..f3f39e9 100644 --- a/src/static/js/workflows/workflow.js +++ b/src/static/js/workflows/workflow.js @@ -242,5 +242,12 @@ class Workflow { } function apiError(info) { - alert("Unable to fetch " + info +". Please try again later or contact support.") + showError("Unable to fetch " + info +". Please try again later or contact support.") } + +function showError(message) { + const text = document.getElementById('alert_modal_message'); + + text.innerText = message; + $("#alert_modal").modal('show'); +} diff --git a/src/templates/base/account/settings.html b/src/templates/base/account/settings.html new file mode 100644 index 0000000..d1939b7 --- /dev/null +++ b/src/templates/base/account/settings.html @@ -0,0 +1,88 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load bootstrap4 %} +{% block content %} +<h1>Settings</h1> + <form action="/accounts/settings/" method="post"> + {% csrf_token %} + <input id="hidden_key_list" type="hidden" name="ssh_key_list" value=""> + <div class="form-group"> + {{ company_form }} + {{ preference_form }} + <br> + <label>SSH Keys:</label> + <ul class="list-group" id="key_ul"> + {% for key in existing_keys %} + <li class="card w-25 mb-1"> + <div class="card-body" style="height: 150px; overflow-y: auto;"> + {{key}} + </div> + <div class="card-footer d-flex flex-row-reverse"> + <div class="btn btn-danger" onclick="remove_key('{{key}}', this.parentNode.parentNode)">Delete</div> + </div> + </li> + {% endfor %} + </ul> + <li class="card w-25 text-truncate mb-1"> + <div class="card-body"> + <textarea id="new_key_area" placeholder="New SSH Public Key" class="form-control" id="new_key_input"></textarea> </div> + <div class="card-footer d-flex flex-row-reverse"> + <div class="btn btn-success" onclick="add_key(this.parentNode.parentNode.childNodes[1].childNodes[1].value)">Add</div> + </div> + </li> + <input class="btn btn-success mt-5" style="width: 100px" name="submitButton" type="submit" value="Save"> + </div> + </form> + +<script> +let key_list = [] +$(window).on('load', function() { + document.getElementById('new_key_area').value = ""; + {% for key in existing_keys %} + key_list.push('{{key}}') + {% endfor %} + update_json_list() + console.log(key_list) +}); + + +function remove_key(target_key, node) { + key_list = key_list.filter(key => key != target_key); + node.setAttribute("hidden", "true"); + update_json_list() +} + +function add_key(key_string) { + console.log(key_string) + if (key_list.indexOf(key_string) != -1) { + alert('This key has already been added'); + return; + } + key_list.push(key_string) + create_key_card(key_string) + update_json_list() +} + +function create_key_card(key_string) { + const elem = document.createElement('li'); + elem.classList.add('card', 'w-25', 'mb-1'); + elem.innerHTML = ` + <div class="card-body" style="height: 150px; overflow-y: auto;"> + ` + key_string + ` + </div> + <div class="card-footer d-flex flex-row-reverse"> + <div class="btn btn-danger" onclick="remove_key('` + key_string +`', this.parentNode.parentNode)">Delete</div> + </div> + ` + + document.getElementById('key_ul').appendChild(elem); + document.getElementById('new_key_area').value = ""; + +} + +function update_json_list() { + document.getElementById("hidden_key_list").value = key_list.toString() +} +</script> +{% endblock content %} + diff --git a/src/templates/base/account/user_list.html b/src/templates/base/account/user_list.html deleted file mode 100644 index e564524..0000000 --- a/src/templates/base/account/user_list.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends "dashboard/table.html" %} -{% load staticfiles %} - -{% block table %} - <thead> - <tr> - <th>Username</th> - <th>Full Name</th> - <th>Email</th> - <th>Company</th> - <th>SSH Key</th> - <th>GPG Key</th> - </tr> - </thead> - <tbody> - {% for user in users %} - <tr> - <td> - {{ user.username }} - </td> - <td> - {{ user.userprofile.full_name }} - </td> - <td> - {{ user.userprofile.email_addr }} - </td> - <td> - {{ user.userprofile.company }} - </td> - <td> - {% if user.userprofile.ssh_public_key %} - <a href={{ user.userprofile.ssh_public_key.url }}>SSH</a> - {% endif %} - </td> - <td> - {% if user.userprofile.pgp_public_key %} - <a href={{ user.userprofile.pgp_public_key.url }}>GPG</a> - {% endif %} - </td> - </tr> - {% endfor %} - </tbody> -{% endblock table %} - - -{% block tablejs %} - <script type="text/javascript"> - $(document).ready(function () { - $('#table').DataTable({ - scrollX: true, - "order": [[0, "asc"]] - }); - }); - </script> -{% endblock tablejs %} diff --git a/src/templates/base/dashboard/landing.html b/src/templates/base/dashboard/landing.html index fea4deb..960ad39 100644 --- a/src/templates/base/dashboard/landing.html +++ b/src/templates/base/dashboard/landing.html @@ -1,16 +1,10 @@ {% extends "base.html" %} {% load staticfiles %} - +{% load bootstrap4 %} {% block content %} <div class="text-center"> {% if not request.user.is_anonymous %} - {% if not request.user.userprofile.ssh_public_key %} - <div class="alert alert-danger alertAnuket" role="alert"> - <b>Warning: you need to upload an ssh key under <a href="/accounts/settings" class="inTextLink" >account settings</a> if you wish to - log into the servers you book</b> - </div> - {% endif %} {% else %} {% endif %} </div> @@ -67,10 +61,37 @@ </div> </div> -<div class="hidden_form d-none" id="form_div"> - <form method="post" action="" class="form" id="wf_selection_form"> - {% csrf_token %} - </form> -</div> +<!-- IPA Modal --> +<div class="modal fade" id="ipa-modal" tabindex="-1"> + <div class="modal-dialog modal-xl"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Welcome to LaaS 3.0</h5> + </div> + <div class="modal-body" id="add_resource_modal_body"> + <p>We have made large scale improvements to the dashboard and our host provisioning service to improve your user experience.</p> + <p>{{ ipa_migrator.message }}</p> + <form action="{{ipa_migrator.action}}" method="post"> + {% csrf_token %} + <p class="text-danger">{{error}}</p> + {{ ipa_migrator.form }} + <div class="form-group"> + <input class="btn btn-success" name="submitButton" type="submit" value="{{ipa_migrator.button}}"> + </div> + </form> + </div> + </div> + </div> + </div> + +<script> + +$(window).on('load', function() { + if ({{ipa_migrator.exists}}) { + $('#ipa-modal').modal({backdrop: 'static', keyboard: false}); + $('#ipa-modal').modal('show'); + } + }); +</script> {% endblock content %} diff --git a/src/templates/base/workflow/book_a_pod.html b/src/templates/base/workflow/book_a_pod.html index 7053bfd..7448dc5 100644 --- a/src/templates/base/workflow/book_a_pod.html +++ b/src/templates/base/workflow/book_a_pod.html @@ -98,7 +98,7 @@ </div> </div> <div class="row align-items-center mt-5"> - <button class="btn btn-danger cancel-book-button p-0 mr-2 col-xl-2 col-md-3 col-5" onclick="workflow.onclickCancel()">Cancel</button> + <!-- <button class="btn btn-danger cancel-book-button p-0 mr-2 col-xl-2 col-md-3 col-5" onclick="workflow.onclickCancel()">Cancel</button> --> <button class="btn btn-success cancel-book-button p-0 ml-2 col-xl-2 col-md-3 col-5" onclick="workflow.onclickConfirm()">Book</button> </div> </div> @@ -111,11 +111,60 @@ {% csrf_token %} </form> </div> + +<div class="modal fade" id="ipa-modal" tabindex="-1"> + <div class="modal-dialog modal-xl"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Welcome to LaaS 3.0</h5> + <button class="close" onclick="window.location.href = '../../'"><span aria-hidden="true">×</span></button> + </div> + <div class="modal-body" id="add_resource_modal_body"> + <p>Please update your information before creating a booking.</p> + <form action="{{prereq_validator.action}}" method="post"> + {% csrf_token %} + {{ prereq_validator.form }} + <div class="form-group"> + <input class="btn btn-success" name="submitButton" type="submit" value="Save"> + </div> + </form> + </div> + </div> + </div> +</div> + +<!-- Alert Modal --> +<div class="modal fade" id="alert_modal" tabindex="-1"> + <div class="modal-dialog modal-sm"> + <div class="modal-content"> + <div class="modal-header"> + <button class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button> + </div> + <div class="modal-body text-center"> + <h5 id="alert_modal_message"></h5> + </div> + <div class="modal-footer d-flex justify-content-center"> + <button class="btn btn-success" data-dismiss="modal" id="alert-modal-submit" onclick="">Confirm</button> + </div> + </div> + </div> +</div> </body> <script> - const user = "{{user}}" - const workflow = new BookingWorkflow(); - workflow.startWorkflow(); + let user; + let workflow; + $(window).on('load', function() { + if ({{prereq_validator.exists}}) { + $('#ipa-modal').modal({backdrop: 'static', keyboard: false}); + $('#ipa-modal').modal('show'); + } else { + user = "{{user}}" + workflow = new BookingWorkflow(); + workflow.startWorkflow(); + } + }); + + </script> {% endblock %} diff --git a/src/templates/base/workflow/design_a_pod.html b/src/templates/base/workflow/design_a_pod.html index 5d48273..ab3f11b 100644 --- a/src/templates/base/workflow/design_a_pod.html +++ b/src/templates/base/workflow/design_a_pod.html @@ -83,14 +83,14 @@ <textarea id="pod-desc-input" class="form-control form-control-lg border border-dark pt-3 pl-3" rows="5" placeholder="Pod Description"></textarea> </div> </div> - <div class="row align-items-center my-4"> + <!-- <div class="row align-items-center my-4"> <div class="col-xl-6 col-md-8 col-11"> <div class="custom-control custom-switch"> <input type="checkbox" class="custom-control-input" id="pod-public-input"> <label class="custom-control-label" for="pod-public-input">Make pod template public?</label> </div> </div> - </div> + </div> --> <div class="row align-items-center my-4"> <div class="col-xl-6 col-md-8 col-11"> <span id="pod_details_error" class="text-danger"></span> @@ -120,7 +120,7 @@ </div> </div> <div class="row align-items-center mt-5"> - <div class="col-xl-2 col-md-3 col-5"><button class="btn btn-danger cancel-book-button p-0 w-100" onclick="workflow.onclickDiscardTemplate()">Discard</button></div> + <!-- <div class="col-xl-2 col-md-3 col-5"><button class="btn btn-danger cancel-book-button p-0 w-100" onclick="workflow.onclickDiscardTemplate()">Discard</button></div> --> <div class="col-xl-2 col-md-3 col-5"><button class="btn btn-success cancel-book-button p-0 w-100" onclick = "workflow.onclickSubmitTemplate()">Create</button></div> </div> </div> @@ -197,6 +197,23 @@ </div> </div> +<!-- Alert Modal --> +<div class="modal fade" id="alert_modal" tabindex="-1"> + <div class="modal-dialog modal-sm"> + <div class="modal-content"> + <div class="modal-header"> + <button class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button> + </div> + <div class="modal-body text-center"> + <h5 id="alert_modal_message"></h5> + </div> + <div class="modal-footer d-flex justify-content-center"> + <button class="btn btn-success" data-dismiss="modal" id="alert-modal-submit" onclick="">Confirm</button> + </div> + </div> + </div> +</div> + <div class="hidden_form d-none"> <form id="token"> {% csrf_token %} diff --git a/src/workflow/README b/src/workflow/README deleted file mode 100644 index 565d1c2..0000000 --- a/src/workflow/README +++ /dev/null @@ -1 +0,0 @@ -TODO: Document how new workflows work diff --git a/src/workflow/views.py b/src/workflow/views.py index 08ed22b..c634b38 100644 --- a/src/workflow/views.py +++ b/src/workflow/views.py @@ -8,12 +8,14 @@ ############################################################################## import json -from django.shortcuts import render +from django.shortcuts import render, redirect from laas_dashboard.settings import TEMPLATE_OVERRIDE from django.http import HttpResponse from django.http.response import JsonResponse from workflow.forms import BookingMetaForm from api.views import liblaas_request, make_booking +from api.utils import get_booking_prereqs_validator +from account.models import UserProfile def no_workflow(request): @@ -27,6 +29,9 @@ def design_a_pod_view(request): if request.method == "GET": if not request.user.is_authenticated: return login(request) + prereq_validator = get_booking_prereqs_validator(UserProfile.objects.get(user=request.user)) + if (prereq_validator["action"] == "no user"): + return redirect("dashboard:index") template = "workflow/design_a_pod.html" context = { "dashboard": str(TEMPLATE_OVERRIDE) @@ -43,10 +48,14 @@ def book_a_pod_view(request): if request.method == "GET": if not request.user.is_authenticated: return login(request) + prereq_validator = get_booking_prereqs_validator(UserProfile.objects.get(user=request.user)) + if (prereq_validator["action"] == "no user"): + return redirect("dashboard:index") template = "workflow/book_a_pod.html" context = { "dashboard": str(TEMPLATE_OVERRIDE), "form": BookingMetaForm(initial={}, user_initial=[], owner=request.user), + "prereq_validator": prereq_validator } return render(request, template, context) |