LFID login for both projects
[laas.git] / src / account / views.py
1 ##############################################################################
2 # Copyright (c) 2016 Max Breitenfeldt and others.
3 # Copyright (c) 2018 Parker Berberian, Sawyer Bergeron, and others.
4 #
5 # All rights reserved. This program and the accompanying materials
6 # are made available under the terms of the Apache License, Version 2.0
7 # which accompanies this distribution, and is available at
8 # http://www.apache.org/licenses/LICENSE-2.0
9 ##############################################################################
10
11
12 import os
13 import urllib
14
15 import oauth2 as oauth
16 from django.conf import settings
17 from django.utils import timezone
18 from django.contrib import messages
19 from django.contrib.auth import logout, authenticate, login
20 from django.contrib.auth.decorators import login_required
21 from django.contrib.auth.mixins import LoginRequiredMixin
22 from django.contrib.auth.models import User
23 from django.urls import reverse
24 from django.http import HttpResponse
25 from django.shortcuts import get_object_or_404
26 from django.utils.decorators import method_decorator
27 from django.views.generic import RedirectView, TemplateView, UpdateView
28 from django.shortcuts import render
29 from jira import JIRA
30 from rest_framework.authtoken.models import Token
31 from mozilla_django_oidc.auth import OIDCAuthenticationBackend
32
33
34 from account.forms import AccountSettingsForm
35 from account.jira_util import SignatureMethod_RSA_SHA1
36 from account.models import UserProfile
37 from booking.models import Booking
38 from resource_inventory.models import ResourceTemplate, Image
39
40
41 @method_decorator(login_required, name='dispatch')
42 class AccountSettingsView(UpdateView):
43     model = UserProfile
44     form_class = AccountSettingsForm
45     template_name_suffix = '_update_form'
46
47     def get_success_url(self):
48         messages.add_message(self.request, messages.INFO,
49                              'Settings saved')
50         return '/'
51
52     def get_object(self, queryset=None):
53         return self.request.user.userprofile
54
55     def get_context_data(self, **kwargs):
56         token, created = Token.objects.get_or_create(user=self.request.user)
57         context = super(AccountSettingsView, self).get_context_data(**kwargs)
58         context.update({'title': "Settings", 'token': token})
59         return context
60
61
62 class MyOIDCAB(OIDCAuthenticationBackend):
63     def filter_users_by_claims(self, claims):
64         """
65         Checks to see if user exists and create user if not
66
67         Linux foundation does not allow users to change their
68         username, so chose to match users based on their username.
69         If this changes we will need to match users based on some
70         other criterea.
71         """
72         username = claims.get(os.environ['CLAIMS_ENDPOINT'] + 'username')
73
74         if not username:
75             return HttpResponse('No username provided, contact support.')
76
77         try:
78             # For literally no (good) reason user needs to be a queryset
79             user = User.objects.filter(username=username)
80             return user
81         except User.DoesNotExist:
82             return self.UserModel.objects.none()
83
84     def create_user(self, claims):
85         """ This creates a user and user profile"""
86         user = super(MyOIDCAB, self).create_user(claims)
87         user.username = claims.get(os.environ['CLAIMS_ENDPOINT'] + 'username')
88         user.save()
89
90         up = UserProfile()
91         up.user = user
92         up.email_addr = claims.get('email')
93         up.save()
94         return user
95
96     def update_user(self, user, claims):
97         """ If their account has different email, change the email """
98         up = UserProfile.objects.get(user=user)
99         up.email_addr = claims.get('email')
100         up.save()
101         return user
102
103
104 class JiraLoginView(RedirectView):
105     def get_redirect_url(self, *args, **kwargs):
106         consumer = oauth.Consumer(settings.OAUTH_CONSUMER_KEY, settings.OAUTH_CONSUMER_SECRET)
107         client = oauth.Client(consumer)
108         client.set_signature_method(SignatureMethod_RSA_SHA1())
109
110         # Step 1. Get a request token from Jira.
111         try:
112             resp, content = client.request(settings.OAUTH_REQUEST_TOKEN_URL, "POST")
113         except Exception:
114             messages.add_message(self.request, messages.ERROR,
115                                  'Error: Connection to Jira failed. Please contact an Administrator')
116             return '/'
117         if resp['status'] != '200':
118             messages.add_message(self.request, messages.ERROR,
119                                  'Error: Connection to Jira failed. Please contact an Administrator')
120             return '/'
121
122         # Step 2. Store the request token in a session for later use.
123         self.request.session['request_token'] = dict(urllib.parse.parse_qsl(content.decode()))
124         # Step 3. Redirect the user to the authentication URL.
125         url = settings.OAUTH_AUTHORIZE_URL + '?oauth_token=' + \
126             self.request.session['request_token']['oauth_token'] + \
127             '&oauth_callback=' + settings.OAUTH_CALLBACK_URL
128         return url
129
130
131 class JiraLogoutView(LoginRequiredMixin, RedirectView):
132     def get_redirect_url(self, *args, **kwargs):
133         logout(self.request)
134         return '/'
135
136
137 class JiraAuthenticatedView(RedirectView):
138     def get_redirect_url(self, *args, **kwargs):
139         # Step 1. Use the request token in the session to build a new client.
140         consumer = oauth.Consumer(settings.OAUTH_CONSUMER_KEY, settings.OAUTH_CONSUMER_SECRET)
141         token = oauth.Token(self.request.session['request_token']['oauth_token'],
142                             self.request.session['request_token']['oauth_token_secret'])
143         client = oauth.Client(consumer, token)
144         client.set_signature_method(SignatureMethod_RSA_SHA1())
145
146         # Step 2. Request the authorized access token from Jira.
147         try:
148             resp, content = client.request(settings.OAUTH_ACCESS_TOKEN_URL, "POST")
149         except Exception:
150             messages.add_message(self.request, messages.ERROR,
151                                  'Error: Connection to Jira failed. Please contact an Administrator')
152             return '/'
153         if resp['status'] != '200':
154             messages.add_message(self.request, messages.ERROR,
155                                  'Error: Connection to Jira failed. Please contact an Administrator')
156             return '/'
157
158         access_token = dict(urllib.parse.parse_qsl(content.decode()))
159
160         module_dir = os.path.dirname(__file__)  # get current directory
161         with open(module_dir + '/rsa.pem', 'r') as f:
162             key_cert = f.read()
163
164         oauth_dict = {
165             'access_token': access_token['oauth_token'],
166             'access_token_secret': access_token['oauth_token_secret'],
167             'consumer_key': settings.OAUTH_CONSUMER_KEY,
168             'key_cert': key_cert
169         }
170
171         jira = JIRA(server=settings.JIRA_URL, oauth=oauth_dict)
172         username = jira.current_user()
173         email = ""
174         try:
175             email = jira.user(username).emailAddress
176         except AttributeError:
177             email = ""
178         url = '/'
179         # Step 3. Lookup the user or create them if they don't exist.
180         try:
181             user = User.objects.get(username=username)
182         except User.DoesNotExist:
183             # Save our permanent token and secret for later.
184             user = User.objects.create_user(username=username,
185                                             password=access_token['oauth_token_secret'])
186             profile = UserProfile()
187             profile.user = user
188             profile.save()
189             user.userprofile.email_addr = email
190             url = reverse('account:settings')
191         user.userprofile.oauth_token = access_token['oauth_token']
192         user.userprofile.oauth_secret = access_token['oauth_token_secret']
193         user.userprofile.save()
194         user.set_password(access_token['oauth_token_secret'])
195         user.save()
196         user = authenticate(username=username, password=access_token['oauth_token_secret'])
197         login(self.request, user)
198         # redirect user to settings page to complete profile
199         return url
200
201
202 @method_decorator(login_required, name='dispatch')
203 class UserListView(TemplateView):
204     template_name = "account/user_list.html"
205
206     def get_context_data(self, **kwargs):
207         users = User.objects.all()
208         context = super(UserListView, self).get_context_data(**kwargs)
209         context.update({'title': "Dashboard Users", 'users': users})
210         return context
211
212
213 def account_detail_view(request):
214     template = "account/details.html"
215     return render(request, template)
216
217
218 def account_resource_view(request):
219     """
220     Display a user's resources.
221
222     gathers a users genericResoureBundles and
223     turns them into displayable objects
224     """
225     if not request.user.is_authenticated:
226         return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
227     template = "account/resource_list.html"
228
229     active_bundles = [book.resource for book in Booking.objects.filter(
230         owner=request.user, end__gte=timezone.now())]
231     active_resources = [bundle.template.id for bundle in active_bundles]
232     resource_list = list(ResourceTemplate.objects.filter(owner=request.user))
233
234     context = {
235         "resources": resource_list,
236         "active_resources": active_resources,
237         "title": "My Resources"
238     }
239     return render(request, template, context=context)
240
241
242 def account_booking_view(request):
243     if not request.user.is_authenticated:
244         return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
245     template = "account/booking_list.html"
246     bookings = list(Booking.objects.filter(owner=request.user, end__gt=timezone.now()).order_by("-start"))
247     my_old_bookings = Booking.objects.filter(owner=request.user, end__lt=timezone.now()).order_by("-start")
248     collab_old_bookings = request.user.collaborators.filter(end__lt=timezone.now()).order_by("-start")
249     expired_bookings = list(my_old_bookings.union(collab_old_bookings))
250     collab_bookings = list(request.user.collaborators.filter(end__gt=timezone.now()).order_by("-start"))
251     context = {
252         "title": "My Bookings",
253         "bookings": bookings,
254         "collab_bookings": collab_bookings,
255         "expired_bookings": expired_bookings
256     }
257     return render(request, template, context=context)
258
259
260 def account_configuration_view(request):
261     if not request.user.is_authenticated:
262         return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
263     template = "account/configuration_list.html"
264     configs = list(ResourceTemplate.objects.filter(owner=request.user))
265     context = {"title": "Configuration List", "configurations": configs}
266     return render(request, template, context=context)
267
268
269 def account_images_view(request):
270     if not request.user.is_authenticated:
271         return render(request, "dashboard/login.html", {'title': 'Authentication Required'})
272     template = "account/image_list.html"
273     my_images = Image.objects.filter(owner=request.user)
274     public_images = Image.objects.filter(public=True)
275     used_images = {}
276     for image in my_images:
277         if image.in_use():
278             used_images[image.id] = "true"
279     context = {
280         "title": "Images",
281         "images": my_images,
282         "public_images": public_images,
283         "used_images": used_images
284     }
285     return render(request, template, context=context)
286
287
288 def resource_delete_view(request, resource_id=None):
289     if not request.user.is_authenticated:
290         return HttpResponse('no')  # 403?
291     grb = get_object_or_404(ResourceTemplate, pk=resource_id)
292     if not request.user.id == grb.owner.id:
293         return HttpResponse('no')  # 403?
294     if Booking.objects.filter(resource__template=grb, end__gt=timezone.now()).exists():
295         return HttpResponse('no')  # 403?
296     grb.delete()
297     return HttpResponse('')
298
299
300 def configuration_delete_view(request, config_id=None):
301     if not request.user.is_authenticated:
302         return HttpResponse('no')  # 403?
303     config = get_object_or_404(ResourceTemplate, pk=config_id)
304     if not request.user.id == config.owner.id:
305         return HttpResponse('no')  # 403?
306     if Booking.objects.filter(resource__template=config, end__gt=timezone.now()).exists():
307         return HttpResponse('no')
308     config.delete()
309     return HttpResponse('')
310
311
312 def booking_cancel_view(request, booking_id=None):
313     if not request.user.is_authenticated:
314         return HttpResponse('no')  # 403?
315     booking = get_object_or_404(Booking, pk=booking_id)
316     if not request.user.id == booking.owner.id:
317         return HttpResponse('no')  # 403?
318
319     if booking.end < timezone.now():  # booking already over
320         return HttpResponse('')
321
322     booking.end = timezone.now()
323     booking.save()
324     return HttpResponse('')
325
326
327 def image_delete_view(request, image_id=None):
328     if not request.user.is_authenticated:
329         return HttpResponse('no')  # 403?
330     image = get_object_or_404(Image, pk=image_id)
331     if image.public or image.owner.id != request.user.id:
332         return HttpResponse('no')  # 403?
333     # check if used in booking
334     if image.in_use():
335         return HttpResponse('no')  # 403?
336     image.delete()
337     return HttpResponse('')