aboutsummaryrefslogtreecommitdiffstats
path: root/src/account
diff options
context:
space:
mode:
Diffstat (limited to 'src/account')
-rw-r--r--src/account/__init__.py8
-rw-r--r--src/account/admin.py19
-rw-r--r--src/account/apps.py15
-rw-r--r--src/account/forms.py22
-rw-r--r--src/account/jira_util.py65
-rw-r--r--src/account/middleware.py32
-rw-r--r--src/account/migrations/0001_initial.py65
-rw-r--r--src/account/migrations/0002_lab_description.py19
-rw-r--r--src/account/migrations/0003_publicnetwork.py25
-rw-r--r--src/account/migrations/__init__.py0
-rw-r--r--src/account/models.py170
-rw-r--r--src/account/tasks.py34
-rw-r--r--src/account/tests/__init__.py8
-rw-r--r--src/account/tests/test_general.py58
-rw-r--r--src/account/urls.py63
-rw-r--r--src/account/views.py292
16 files changed, 895 insertions, 0 deletions
diff --git a/src/account/__init__.py b/src/account/__init__.py
new file mode 100644
index 0000000..b6fef6c
--- /dev/null
+++ b/src/account/__init__.py
@@ -0,0 +1,8 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
diff --git a/src/account/admin.py b/src/account/admin.py
new file mode 100644
index 0000000..b4c142c
--- /dev/null
+++ b/src/account/admin.py
@@ -0,0 +1,19 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.contrib import admin
+
+from account.models import UserProfile, Lab, VlanManager, PublicNetwork
+
+admin.site.register(UserProfile)
+admin.site.register(Lab)
+admin.site.register(VlanManager)
+admin.site.register(PublicNetwork)
diff --git a/src/account/apps.py b/src/account/apps.py
new file mode 100644
index 0000000..9814648
--- /dev/null
+++ b/src/account/apps.py
@@ -0,0 +1,15 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+ name = 'account'
diff --git a/src/account/forms.py b/src/account/forms.py
new file mode 100644
index 0000000..3b9c627
--- /dev/null
+++ b/src/account/forms.py
@@ -0,0 +1,22 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import django.forms as forms
+import pytz as pytz
+
+from account.models import UserProfile
+
+
+class AccountSettingsForm(forms.ModelForm):
+ class Meta:
+ model = UserProfile
+ fields = ['company', 'email_addr', 'ssh_public_key', 'pgp_public_key', 'timezone']
+
+ timezone = forms.ChoiceField(choices=[(x, x) for x in pytz.common_timezones], initial='UTC')
diff --git a/src/account/jira_util.py b/src/account/jira_util.py
new file mode 100644
index 0000000..18b0e26
--- /dev/null
+++ b/src/account/jira_util.py
@@ -0,0 +1,65 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import base64
+import os
+
+import oauth2 as oauth
+from django.conf import settings
+from jira import JIRA
+from tlslite.utils import keyfactory
+
+
+class SignatureMethod_RSA_SHA1(oauth.SignatureMethod):
+ name = 'RSA-SHA1'
+
+ def signing_base(self, request, consumer, token):
+ if not hasattr(request, 'normalized_url') or request.normalized_url is None:
+ raise ValueError("Base URL for request is not set.")
+
+ sig = (
+ oauth.escape(request.method),
+ oauth.escape(request.normalized_url),
+ oauth.escape(request.get_normalized_parameters()),
+ )
+
+ key = '%s&' % oauth.escape(consumer.secret)
+ if token:
+ key += oauth.escape(token.secret)
+ raw = '&'.join(sig)
+ return key, raw
+
+ def sign(self, request, consumer, token):
+ """Builds the base signature string."""
+ key, raw = self.signing_base(request, consumer, token)
+
+ module_dir = os.path.dirname(__file__) # get current directory
+ with open(module_dir + '/rsa.pem', 'r') as f:
+ data = f.read()
+ privateKeyString = data.strip()
+ privatekey = keyfactory.parsePrivateKey(privateKeyString)
+ raw = str.encode(raw)
+ signature = privatekey.hashAndSign(raw)
+ return base64.b64encode(signature)
+
+
+def get_jira(user):
+ module_dir = os.path.dirname(__file__) # get current directory
+ with open(module_dir + '/rsa.pem', 'r') as f:
+ key_cert = f.read()
+
+ oauth_dict = {
+ 'access_token': user.userprofile.oauth_token,
+ 'access_token_secret': user.userprofile.oauth_secret,
+ 'consumer_key': settings.OAUTH_CONSUMER_KEY,
+ 'key_cert': key_cert
+ }
+
+ return JIRA(server=settings.JIRA_URL, oauth=oauth_dict)
diff --git a/src/account/middleware.py b/src/account/middleware.py
new file mode 100644
index 0000000..0f1dbd8
--- /dev/null
+++ b/src/account/middleware.py
@@ -0,0 +1,32 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.utils import timezone
+from django.utils.deprecation import MiddlewareMixin
+
+from account.models import UserProfile
+
+
+class TimezoneMiddleware(MiddlewareMixin):
+ """
+ Activate the timezone from request.user.userprofile if user is authenticated,
+ deactivate the timezone otherwise and use default (UTC)
+ """
+ def process_request(self, request):
+ if request.user.is_authenticated:
+ try:
+ tz = request.user.userprofile.timezone
+ timezone.activate(tz)
+ except UserProfile.DoesNotExist:
+ UserProfile.objects.create(user=request.user)
+ tz = request.user.userprofile.timezone
+ timezone.activate(tz)
+ else:
+ timezone.deactivate()
diff --git a/src/account/migrations/0001_initial.py b/src/account/migrations/0001_initial.py
new file mode 100644
index 0000000..c8b5bdc
--- /dev/null
+++ b/src/account/migrations/0001_initial.py
@@ -0,0 +1,65 @@
+# Generated by Django 2.1 on 2018-09-14 14:48
+
+import account.models
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Lab',
+ fields=[
+ ('name', models.CharField(max_length=200, primary_key=True, serialize=False, unique=True)),
+ ('contact_email', models.EmailField(blank=True, max_length=200, null=True)),
+ ('contact_phone', models.CharField(blank=True, max_length=20, null=True)),
+ ('status', models.IntegerField(default=0)),
+ ('location', models.TextField(default='unknown')),
+ ('api_token', models.CharField(max_length=50)),
+ ('lab_user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='UserProfile',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('timezone', models.CharField(default='UTC', max_length=100)),
+ ('ssh_public_key', models.FileField(blank=True, null=True, upload_to=account.models.upload_to)),
+ ('pgp_public_key', models.FileField(blank=True, null=True, upload_to=account.models.upload_to)),
+ ('email_addr', models.CharField(default='email@mail.com', max_length=300)),
+ ('company', models.CharField(max_length=200)),
+ ('oauth_token', models.CharField(max_length=1024)),
+ ('oauth_secret', models.CharField(max_length=1024)),
+ ('jira_url', models.CharField(default='', max_length=100)),
+ ('full_name', models.CharField(default='', max_length=100)),
+ ('booking_privledge', models.BooleanField(default=False)),
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'db_table': 'user_profile',
+ },
+ ),
+ migrations.CreateModel(
+ name='VlanManager',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('vlans', models.TextField()),
+ ('block_size', models.IntegerField()),
+ ('allow_overlapping', models.BooleanField()),
+ ('reserved_vlans', models.TextField()),
+ ],
+ ),
+ migrations.AddField(
+ model_name='lab',
+ name='vlan_manager',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='account.VlanManager'),
+ ),
+ ]
diff --git a/src/account/migrations/0002_lab_description.py b/src/account/migrations/0002_lab_description.py
new file mode 100644
index 0000000..445501a
--- /dev/null
+++ b/src/account/migrations/0002_lab_description.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.1 on 2018-09-14 20:22
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('account', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='lab',
+ name='description',
+ field=models.CharField(default='Lab description default', max_length=240),
+ preserve_default=False,
+ ),
+ ]
diff --git a/src/account/migrations/0003_publicnetwork.py b/src/account/migrations/0003_publicnetwork.py
new file mode 100644
index 0000000..71e5caa
--- /dev/null
+++ b/src/account/migrations/0003_publicnetwork.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.1 on 2018-09-26 14:41
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('account', '0002_lab_description'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='PublicNetwork',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('vlan', models.IntegerField()),
+ ('in_use', models.BooleanField(default=False)),
+ ('cidr', models.CharField(default='0.0.0.0/0', max_length=50)),
+ ('gateway', models.CharField(default='0.0.0.0', max_length=50)),
+ ('lab', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='account.Lab')),
+ ],
+ ),
+ ]
diff --git a/src/account/migrations/__init__.py b/src/account/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/account/migrations/__init__.py
diff --git a/src/account/models.py b/src/account/models.py
new file mode 100644
index 0000000..4fc7c40
--- /dev/null
+++ b/src/account/models.py
@@ -0,0 +1,170 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.contrib.auth.models import User
+from django.db import models
+import json
+import random
+
+
+class LabStatus(object):
+ UP = 0
+ TEMP_DOWN = 100
+ DOWN = 200
+
+
+def upload_to(object, filename):
+ return object.user.username + '/' + filename
+
+
+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, default='')
+ full_name = models.CharField(max_length=100, default='')
+ booking_privledge = models.BooleanField(default=False)
+
+ class Meta:
+ db_table = 'user_profile'
+
+ def __str__(self):
+ return self.user.username
+
+
+class VlanManager(models.Model):
+ # list of length 4096 containing either 0 (not available) or 1 (available)
+ vlans = models.TextField()
+ block_size = models.IntegerField()
+ allow_overlapping = models.BooleanField()
+ # list of length 4096 containing either 0 (not rexerved) or 1 (reserved)
+ reserved_vlans = models.TextField()
+
+ def get_vlan(self, count=1):
+ allocated = []
+ vlans = json.loads(self.vlans)
+ for i in range(count):
+ new_vlan = vlans.index(1) # will throw if none available
+ vlans[new_vlan] = 0
+ allocated.append(new_vlan)
+ if count == 1:
+ return allocated[0]
+ return allocated
+
+ def get_public_vlan(self):
+ return PublicNetwork.objects.filter(lab=self.lab_set.first(), in_use=False).first()
+
+ def reserve_public_vlan(self, vlan):
+ net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan, in_use=False)
+ net.in_use = True
+ net.save()
+
+ def release_public_vlan(self, vlan):
+ net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan, in_use=True)
+ net.in_use = False
+ net.save()
+
+ def public_vlan_is_available(self, vlan):
+ net = PublicNetwork.objects.get(lab=self.lab_set.first(), vlan=vlan)
+ return not net.in_use
+
+ def is_available(self, vlans):
+ """
+ 'vlans' is either a single vlan id integer or a list of integers
+ will return true (available) or false
+ """
+ if self.allow_overlapping:
+ return True
+
+ reserved = json.loads(self.reserved_vlans)
+ vlan_master_list = json.loads(self.vlans)
+ try:
+ iter(vlans)
+ except Exception:
+ vlans = [vlans]
+
+ for vlan in vlans:
+ if not vlan_master_list[vlan] or reserved[vlan]:
+ return False
+ return True
+
+ def release_vlans(self, vlans):
+ """
+ 'vlans' is either a single vlan id integer or a list of integers
+ will make the vlans available
+ doesnt return a value
+ """
+ my_vlans = json.loads(self.vlans)
+
+ try:
+ iter(vlans)
+ except Exception:
+ vlans = [vlans]
+
+ for vlan in vlans:
+ my_vlans[vlan] = 1
+ self.vlans = json.dumps(my_vlans)
+ self.save()
+
+ def reserve_vlans(self, vlans):
+ my_vlans = json.loads(self.vlans)
+
+ try:
+ iter(vlans)
+ except Exception:
+ vlans = [vlans]
+
+ vlans = set(vlans)
+
+ for vlan in vlans:
+ if my_vlans[vlan] == 0:
+ raise ValueError("vlan " + str(vlan) + " is not available")
+
+ my_vlans[vlan] = 0
+ self.vlans = json.dumps(my_vlans)
+ self.save()
+
+
+class Lab(models.Model):
+ lab_user = models.OneToOneField(User, on_delete=models.CASCADE)
+ name = models.CharField(max_length=200, primary_key=True, unique=True, null=False, blank=False)
+ contact_email = models.EmailField(max_length=200, null=True, blank=True)
+ contact_phone = models.CharField(max_length=20, null=True, blank=True)
+ status = models.IntegerField(default=LabStatus.UP)
+ vlan_manager = models.ForeignKey(VlanManager, on_delete=models.CASCADE, null=True)
+ location = models.TextField(default="unknown")
+ api_token = models.CharField(max_length=50)
+ description = models.CharField(max_length=240)
+
+ @staticmethod
+ def make_api_token():
+ alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ key = ""
+ for i in range(45):
+ key += random.choice(alphabet)
+ return key
+
+ def __str__(self):
+ return self.name
+
+
+class PublicNetwork(models.Model):
+ vlan = models.IntegerField()
+ lab = models.ForeignKey(Lab, on_delete=models.CASCADE)
+ in_use = models.BooleanField(default=False)
+ cidr = models.CharField(max_length=50, default="0.0.0.0/0")
+ gateway = models.CharField(max_length=50, default="0.0.0.0")
diff --git a/src/account/tasks.py b/src/account/tasks.py
new file mode 100644
index 0000000..fe51974
--- /dev/null
+++ b/src/account/tasks.py
@@ -0,0 +1,34 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from celery import shared_task
+from django.contrib.auth.models import User
+from jira import JIRAError
+
+from account.jira_util import get_jira
+
+
+@shared_task
+def sync_jira_accounts():
+ users = User.objects.all()
+ for user in users:
+ jira = get_jira(user)
+ try:
+ user_dict = jira.myself()
+ except JIRAError:
+ # User can be anonymous (local django admin account)
+ continue
+ user.email = user_dict['emailAddress']
+ user.userprofile.url = user_dict['self']
+ user.userprofile.full_name = user_dict['displayName']
+
+ user.userprofile.save()
+ user.save()
diff --git a/src/account/tests/__init__.py b/src/account/tests/__init__.py
new file mode 100644
index 0000000..b6fef6c
--- /dev/null
+++ b/src/account/tests/__init__.py
@@ -0,0 +1,8 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
diff --git a/src/account/tests/test_general.py b/src/account/tests/test_general.py
new file mode 100644
index 0000000..3fb52b0
--- /dev/null
+++ b/src/account/tests/test_general.py
@@ -0,0 +1,58 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+from django.contrib.auth.models import User
+from django.test import Client
+from django.test import TestCase
+from django.urls import reverse
+from django.utils import timezone
+
+from account.models import UserProfile
+
+
+class AccountMiddlewareTestCase(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.user1 = User.objects.create(username='user1')
+ self.user1.set_password('user1')
+ self.user1profile = UserProfile.objects.create(user=self.user1)
+ self.user1.save()
+
+ def test_timezone_middleware(self):
+ """
+ The timezone should be UTC for anonymous users, for authenticated users it should be set
+ to user.userprofile.timezone
+ """
+ # default
+ self.assertEqual(timezone.get_current_timezone_name(), 'UTC')
+
+ url = reverse('account:settings')
+ # anonymous request
+ self.client.get(url)
+ self.assertEqual(timezone.get_current_timezone_name(), 'UTC')
+
+ # authenticated user with UTC timezone (userprofile default)
+ self.client.login(username='user1', password='user1')
+ self.client.get(url)
+ self.assertEqual(timezone.get_current_timezone_name(), 'UTC')
+
+ # authenticated user with custom timezone (userprofile default)
+ self.user1profile.timezone = 'Etc/Greenwich'
+ self.user1profile.save()
+ self.client.get(url)
+ self.assertEqual(timezone.get_current_timezone_name(), 'GMT')
+
+ # if there is no profile for a user, it should be created
+ user2 = User.objects.create(username='user2')
+ user2.set_password('user2')
+ user2.save()
+ self.client.login(username='user2', password='user2')
+ self.client.get(url)
+ self.assertTrue(user2.userprofile)
diff --git a/src/account/urls.py b/src/account/urls.py
new file mode 100644
index 0000000..8aad80c
--- /dev/null
+++ b/src/account/urls.py
@@ -0,0 +1,63 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+"""pharos_dashboard URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/1.10/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
+Including another URLconf
+ 1. Import the include() function: from django.conf.urls import url, include
+ 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
+"""
+from django.conf.urls import url
+from django.urls import path
+
+from account.views import (
+ AccountSettingsView,
+ JiraAuthenticatedView,
+ JiraLoginView,
+ JiraLogoutView,
+ UserListView,
+ account_resource_view,
+ account_booking_view,
+ account_images_view,
+ account_configuration_view,
+ account_detail_view,
+ resource_delete_view,
+ booking_cancel_view,
+ image_delete_view,
+ configuration_delete_view
+)
+
+app_name = "account"
+urlpatterns = [
+ url(r'^settings/', AccountSettingsView.as_view(), name='settings'),
+ url(r'^authenticated/$', JiraAuthenticatedView.as_view(), name='authenticated'),
+ url(r'^login/$', JiraLoginView.as_view(), name='login'),
+ url(r'^logout/$', JiraLogoutView.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>', resource_delete_view),
+ url(r'^my/bookings/$', account_booking_view, name="my-bookings"),
+ path('my/bookings/cancel/<int:booking_id>', booking_cancel_view),
+ url(r'^my/images/$', account_images_view, name="my-images"),
+ path('my/images/delete/<int:image_id>', image_delete_view),
+ url(r'^my/configurations/$', account_configuration_view, name="my-configurations"),
+ path('my/configurations/delete/<int:config_id>', configuration_delete_view),
+ url(r'^my/$', account_detail_view, name="my-account"),
+]
diff --git a/src/account/views.py b/src/account/views.py
new file mode 100644
index 0000000..2b4eccb
--- /dev/null
+++ b/src/account/views.py
@@ -0,0 +1,292 @@
+##############################################################################
+# Copyright (c) 2016 Max Breitenfeldt and others.
+# Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+
+import os
+import urllib
+
+import oauth2 as oauth
+from django.conf import settings
+from django.utils import timezone
+from django.contrib import messages
+from django.contrib.auth import logout, authenticate, login
+from django.contrib.auth.decorators import login_required
+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.utils.decorators import method_decorator
+from django.views.generic import RedirectView, TemplateView, UpdateView
+from django.shortcuts import render
+from jira import JIRA
+from rest_framework.authtoken.models import Token
+
+from account.forms import AccountSettingsForm
+from account.jira_util import SignatureMethod_RSA_SHA1
+from account.models import UserProfile
+from booking.models import Booking
+from resource_inventory.models import GenericResourceBundle, ConfigBundle, Image, Host
+
+
+@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 '/'
+
+ def get_object(self, queryset=None):
+ return self.request.user.userprofile
+
+ 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
+
+
+class JiraLoginView(RedirectView):
+ def get_redirect_url(self, *args, **kwargs):
+ consumer = oauth.Consumer(settings.OAUTH_CONSUMER_KEY, settings.OAUTH_CONSUMER_SECRET)
+ client = oauth.Client(consumer)
+ client.set_signature_method(SignatureMethod_RSA_SHA1())
+
+ # Step 1. Get a request token from Jira.
+ try:
+ resp, content = client.request(settings.OAUTH_REQUEST_TOKEN_URL, "POST")
+ except Exception:
+ messages.add_message(self.request, messages.ERROR,
+ 'Error: Connection to Jira failed. Please contact an Administrator')
+ return '/'
+ if resp['status'] != '200':
+ messages.add_message(self.request, messages.ERROR,
+ 'Error: Connection to Jira failed. Please contact an Administrator')
+ return '/'
+
+ # Step 2. Store the request token in a session for later use.
+ self.request.session['request_token'] = dict(urllib.parse.parse_qsl(content.decode()))
+ # Step 3. Redirect the user to the authentication URL.
+ url = settings.OAUTH_AUTHORIZE_URL + '?oauth_token=' + \
+ self.request.session['request_token']['oauth_token'] + \
+ '&oauth_callback=' + settings.OAUTH_CALLBACK_URL
+ return url
+
+
+class JiraLogoutView(LoginRequiredMixin, RedirectView):
+ def get_redirect_url(self, *args, **kwargs):
+ logout(self.request)
+ return '/'
+
+
+class JiraAuthenticatedView(RedirectView):
+ def get_redirect_url(self, *args, **kwargs):
+ # Step 1. Use the request token in the session to build a new client.
+ consumer = oauth.Consumer(settings.OAUTH_CONSUMER_KEY, settings.OAUTH_CONSUMER_SECRET)
+ token = oauth.Token(self.request.session['request_token']['oauth_token'],
+ self.request.session['request_token']['oauth_token_secret'])
+ client = oauth.Client(consumer, token)
+ client.set_signature_method(SignatureMethod_RSA_SHA1())
+
+ # Step 2. Request the authorized access token from Jira.
+ try:
+ resp, content = client.request(settings.OAUTH_ACCESS_TOKEN_URL, "POST")
+ except Exception:
+ messages.add_message(self.request, messages.ERROR,
+ 'Error: Connection to Jira failed. Please contact an Administrator')
+ return '/'
+ if resp['status'] != '200':
+ messages.add_message(self.request, messages.ERROR,
+ 'Error: Connection to Jira failed. Please contact an Administrator')
+ return '/'
+
+ access_token = dict(urllib.parse.parse_qsl(content.decode()))
+
+ module_dir = os.path.dirname(__file__) # get current directory
+ with open(module_dir + '/rsa.pem', 'r') as f:
+ key_cert = f.read()
+
+ oauth_dict = {
+ 'access_token': access_token['oauth_token'],
+ 'access_token_secret': access_token['oauth_token_secret'],
+ 'consumer_key': settings.OAUTH_CONSUMER_KEY,
+ 'key_cert': key_cert
+ }
+
+ jira = JIRA(server=settings.JIRA_URL, oauth=oauth_dict)
+ username = jira.current_user()
+ email = jira.user(username).emailAddress
+ url = '/'
+ # Step 3. Lookup the user or create them if they don't exist.
+ try:
+ user = User.objects.get(username=username)
+ except User.DoesNotExist:
+ # Save our permanent token and secret for later.
+ user = User.objects.create_user(username=username,
+ password=access_token['oauth_token_secret'])
+ profile = UserProfile()
+ profile.user = user
+ profile.save()
+ user.userprofile.email_addr = email
+ url = reverse('account:settings')
+ user.userprofile.oauth_token = access_token['oauth_token']
+ user.userprofile.oauth_secret = access_token['oauth_token_secret']
+ user.userprofile.save()
+ user.set_password(access_token['oauth_token_secret'])
+ user.save()
+ user = authenticate(username=username, password=access_token['oauth_token_secret'])
+ login(self.request, user)
+ # redirect user to settings page to complete profile
+ return url
+
+
+@method_decorator(login_required, name='dispatch')
+class UserListView(TemplateView):
+ template_name = "account/user_list.html"
+
+ def get_context_data(self, **kwargs):
+ users = User.objects.all()
+ 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)
+
+
+def account_resource_view(request):
+ """
+ gathers a users genericResoureBundles and
+ turns them into displayable objects
+ """
+ if not request.user.is_authenticated:
+ return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
+ template = "account/resource_list.html"
+ resources = GenericResourceBundle.objects.filter(
+ owner=request.user).prefetch_related("configbundle_set")
+ mapping = {}
+ resource_list = []
+ booking_mapping = {}
+ for grb in resources:
+ resource_list.append(grb)
+ mapping[grb.id] = [{"id": x.id, "name": x.name} for x in grb.configbundle_set.all()]
+ if Booking.objects.filter(resource__template=grb, end__gt=timezone.now()).exists():
+ booking_mapping[grb.id] = "true"
+ context = {
+ "resources": resource_list,
+ "grb_mapping": mapping,
+ "booking_mapping": booking_mapping,
+ "title": "My Resources"
+ }
+ return render(request, template, context=context)
+
+
+def account_booking_view(request):
+ if not request.user.is_authenticated:
+ return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
+ 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")
+ collab_old_bookings = request.user.collaborators.filter(end__lt=timezone.now()).order_by("-start")
+ expired_bookings = list(my_old_bookings.union(collab_old_bookings))
+ collab_bookings = list(request.user.collaborators.filter(end__gt=timezone.now()).order_by("-start"))
+ context = {
+ "title": "My Bookings",
+ "bookings": bookings,
+ "collab_bookings": collab_bookings,
+ "expired_bookings": expired_bookings
+ }
+ return render(request, template, context=context)
+
+
+def account_configuration_view(request):
+ if not request.user.is_authenticated:
+ return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
+ template = "account/configuration_list.html"
+ configs = list(ConfigBundle.objects.filter(owner=request.user))
+ context = {"title": "Configuration List", "configurations": configs}
+ return render(request, template, context=context)
+
+
+def account_images_view(request):
+ if not request.user.is_authenticated:
+ return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
+ template = "account/image_list.html"
+ my_images = Image.objects.filter(owner=request.user)
+ public_images = Image.objects.filter(public=True)
+ used_images = {}
+ for image in my_images:
+ if Host.objects.filter(booked=True, config__image=image).exists():
+ used_images[image.id] = "true"
+ context = {
+ "title": "Images",
+ "images": my_images,
+ "public_images": public_images,
+ "used_images": used_images
+ }
+ return render(request, template, context=context)
+
+
+def resource_delete_view(request, resource_id=None):
+ if not request.user.is_authenticated:
+ return HttpResponse('no') # 403?
+ grb = get_object_or_404(GenericResourceBundle, pk=resource_id)
+ if not request.user.id == grb.owner.id:
+ return HttpResponse('no') # 403?
+ if Booking.objects.filter(resource__template=grb, end__gt=timezone.now()).exists():
+ return HttpResponse('no') # 403?
+ grb.delete()
+ return HttpResponse('')
+
+
+def configuration_delete_view(request, config_id=None):
+ if not request.user.is_authenticated:
+ return HttpResponse('no') # 403?
+ config = get_object_or_404(ConfigBundle, pk=config_id)
+ if not request.user.id == config.owner.id:
+ return HttpResponse('no') # 403?
+ if Booking.objects.filter(config_bundle=config, end__gt=timezone.now()).exists():
+ return HttpResponse('no')
+ config.delete()
+ return HttpResponse('')
+
+
+def booking_cancel_view(request, booking_id=None):
+ if not request.user.is_authenticated:
+ return HttpResponse('no') # 403?
+ booking = get_object_or_404(Booking, pk=booking_id)
+ if not request.user.id == booking.owner.id:
+ return HttpResponse('no') # 403?
+
+ if booking.end < timezone.now(): # booking already over
+ return HttpResponse('')
+
+ booking.end = timezone.now()
+ booking.save()
+ return HttpResponse('')
+
+
+def image_delete_view(request, image_id=None):
+ if not request.user.is_authenticated:
+ return HttpResponse('no') # 403?
+ image = get_object_or_404(Image, pk=image_id)
+ if image.public or image.owner.id != request.user.id:
+ return HttpResponse('no') # 403?
+ # check if used in booking
+ if Host.objects.filter(booked=True, config__image=image).exists():
+ return HttpResponse('no') # 403?
+ image.delete()
+ return HttpResponse('')