# Copyright 2014 Budapest University of Technology and Economics (BME IK) # # This file is part of CIRCLE Cloud. # # CIRCLE is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) # any later version. # # CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along # with CIRCLE. If not, see <http://www.gnu.org/licenses/>. from __future__ import unicode_literals, absolute_import import json import logging from django.conf import settings from django.contrib import messages from django.contrib.auth import login from django.contrib.auth.models import User, Group from django.contrib.auth.views import login as login_view from django.contrib.messages.views import SuccessMessageMixin from django.core import signing from django.core.exceptions import ( PermissionDenied, SuspiciousOperation, ) from django.core.urlresolvers import reverse, reverse_lazy from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.shortcuts import redirect, get_object_or_404 from django.utils.translation import ugettext as _ from django.views.decorators.http import require_POST from django.views.generic import ( TemplateView, DetailView, View, DeleteView, UpdateView, CreateView, ) from django_sshkey.models import UserKey from braces.views import LoginRequiredMixin, PermissionRequiredMixin from vm.models import Instance, InstanceTemplate from ..forms import ( CircleAuthenticationForm, MyProfileForm, UserCreationForm, UnsubscribeForm, UserKeyForm, CirclePasswordChangeForm, ConnectCommandForm, ) from ..models import Profile, GroupProfile, ConnectCommand, create_profile from ..tables import UserKeyListTable, ConnectCommandListTable from .util import saml_available logger = logging.getLogger(__name__) class NotificationView(LoginRequiredMixin, TemplateView): def get_template_names(self): if self.request.is_ajax(): return ['dashboard/_notifications-timeline.html'] else: return ['dashboard/notifications.html'] def get_context_data(self, *args, **kwargs): context = super(NotificationView, self).get_context_data( *args, **kwargs) n = 10 if self.request.is_ajax() else 1000 context['notifications'] = list( self.request.user.notification_set.all()[:n]) return context def get(self, *args, **kwargs): response = super(NotificationView, self).get(*args, **kwargs) un = self.request.user.notification_set.filter(status="new") for u in un: u.status = "read" u.save() return response def circle_login(request): authentication_form = CircleAuthenticationForm extra_context = { 'saml2': saml_available, } response = login_view(request, authentication_form=authentication_form, extra_context=extra_context) set_language_cookie(request, response) return response class TokenLogin(View): token_max_age = 120 # seconds @classmethod def get_salt(cls): return unicode(cls) @classmethod def get_token(cls, user, sudoer): return signing.dumps((sudoer.pk, user.pk), salt=cls.get_salt(), compress=True) @classmethod def get_token_url(cls, user, sudoer): key = cls.get_token(user, sudoer) return reverse("dashboard.views.token-login", args=(key, )) def get(self, request, token, *args, **kwargs): try: data = signing.loads(token, salt=self.get_salt(), max_age=self.token_max_age) logger.debug('TokenLogin token data: %s', unicode(data)) sudoer, user = data logger.debug('Extracted TokenLogin data: sudoer: %s, user: %s', unicode(sudoer), unicode(user)) except (signing.BadSignature, ValueError, TypeError) as e: logger.warning('Tried invalid TokenLogin token. ' 'Token: %s, user: %s. %s', token, unicode(self.request.user), unicode(e)) raise SuspiciousOperation() sudoer = User.objects.get(pk=sudoer) if not sudoer.is_superuser: raise PermissionDenied() user = User.objects.get(pk=user) user.backend = 'django.contrib.auth.backends.ModelBackend' logger.warning('%s %d logged in as user %s %d', unicode(sudoer), sudoer.pk, unicode(user), user.pk) login(request, user) messages.info(request, _("Logged in as user %s.") % unicode(user)) return redirect("/") class MyPreferencesView(UpdateView): model = Profile def get_context_data(self, *args, **kwargs): context = super(MyPreferencesView, self).get_context_data(*args, **kwargs) context['forms'] = { 'change_password': CirclePasswordChangeForm( user=self.request.user), 'change_language': MyProfileForm(instance=self.get_object()), } key_table = UserKeyListTable( UserKey.objects.filter(user=self.request.user), request=self.request) key_table.page = None context['userkey_table'] = key_table cmd_table = ConnectCommandListTable( self.request.user.command_set.all(), request=self.request) cmd_table.page = None context['connectcommand_table'] = cmd_table return context def get_object(self, queryset=None): if self.request.user.is_anonymous(): raise PermissionDenied() try: return self.request.user.profile except Profile.DoesNotExist: raise Http404(_("You don't have a profile.")) def post(self, request, *args, **kwargs): self.ojbect = self.get_object() redirect_response = HttpResponseRedirect( reverse("dashboard.views.profile-preferences")) if "preferred_language" in request.POST: form = MyProfileForm(request.POST, instance=self.get_object()) if form.is_valid(): lang = form.cleaned_data.get("preferred_language") set_language_cookie(self.request, redirect_response, lang) form.save() else: form = CirclePasswordChangeForm(user=request.user, data=request.POST) if form.is_valid(): form.save() if form.is_valid(): return redirect_response else: return self.get(request, form=form, *args, **kwargs) def get(self, request, form=None, *args, **kwargs): # if this is not here, it won't work self.object = self.get_object() context = self.get_context_data(*args, **kwargs) if form is not None: # a little cheating, users can't post invalid # language selection forms (without modifying the HTML) context['forms']['change_password'] = form return self.render_to_response(context) class UnsubscribeFormView(SuccessMessageMixin, UpdateView): model = Profile form_class = UnsubscribeForm template_name = "dashboard/unsubscribe.html" success_message = _("Successfully modified subscription.") def get_success_url(self): if self.request.user.is_authenticated(): return super(UnsubscribeFormView, self).get_success_url() else: return self.request.path @classmethod def get_salt(cls): return unicode(cls) @classmethod def get_token(cls, user): return signing.dumps(user.pk, salt=cls.get_salt(), compress=True) def get_object(self, queryset=None): key = self.kwargs['token'] try: pk = signing.loads(key, salt=self.get_salt(), max_age=48 * 3600) except signing.SignatureExpired: raise except (signing.BadSignature, ValueError, TypeError) as e: logger.warning('Tried invalid token. Token: %s, user: %s. %s', key, unicode(self.request.user), unicode(e)) raise Http404 else: return (queryset or self.get_queryset()).get(user_id=pk) def dispatch(self, request, *args, **kwargs): try: return super(UnsubscribeFormView, self).dispatch( request, *args, **kwargs) except signing.SignatureExpired: return redirect('dashboard.views.profile-preferences') def set_language_cookie(request, response, lang=None): if lang is None: try: lang = request.user.profile.preferred_language except: return cname = getattr(settings, 'LANGUAGE_COOKIE_NAME', 'django_language') response.set_cookie(cname, lang, 365 * 86400) class UserCreationView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): form_class = UserCreationForm model = User template_name = 'dashboard/user-create.html' permission_required = "auth.add_user" def get_group(self, group_pk): self.group = get_object_or_404(Group, pk=group_pk) if not self.group.profile.has_level(self.request.user, 'owner'): raise PermissionDenied() def get(self, *args, **kwargs): self.get_group(kwargs.pop('group_pk')) return super(UserCreationView, self).get(*args, **kwargs) def post(self, *args, **kwargs): group_pk = kwargs.pop('group_pk') self.get_group(group_pk) ret = super(UserCreationView, self).post(*args, **kwargs) if self.object: create_profile(self.object) self.object.groups.add(self.group) return redirect( reverse('dashboard.views.group-detail', args=[group_pk])) else: return ret class ProfileView(LoginRequiredMixin, DetailView): template_name = "dashboard/profile.html" model = User slug_field = "username" slug_url_kwarg = "username" def get(self, *args, **kwargs): user = self.request.user target = self.get_object() # get the list of groups where the user is operator user_g_w_op = GroupProfile.get_objects_with_level("operator", user) # get the list of groups the "target" (the profile) is member of target_groups = GroupProfile.objects.filter( group__in=target.groups.all()) intersection = set(user_g_w_op).intersection(target_groups) # if the intersection of the 2 lists is empty the logged in user # has no permission to check the target's profile # (except if the user want to see his own profile) if len(intersection) < 1 and target != user: raise PermissionDenied return super(ProfileView, self).get(*args, **kwargs) def get_context_data(self, **kwargs): context = super(ProfileView, self).get_context_data(**kwargs) user = self.get_object() context['profile'] = user context['avatar_url'] = user.profile.get_avatar_url() context['instances_owned'] = Instance.get_objects_with_level( "owner", user, disregard_superuser=True).filter(destroyed_at=None) context['instances_with_access'] = Instance.get_objects_with_level( "user", user, disregard_superuser=True ).filter(destroyed_at=None).exclude(pk__in=context['instances_owned']) group_profiles = GroupProfile.get_objects_with_level( "operator", self.request.user) groups = Group.objects.filter(groupprofile__in=group_profiles) context['groups'] = user.groups.filter(pk__in=groups) # permissions # show groups only if the user is superuser, or have access # to any of the groups the user belongs to context['perm_group_list'] = ( self.request.user.is_superuser or len(context['groups']) > 0) context['perm_email'] = ( context['perm_group_list'] or self.request.user == user) # filter the virtual machine list # if the logged in user is not superuser or not the user itself # filter the list so only those virtual machines are shown that are # originated from templates the logged in user is operator or higher if not (self.request.user.is_superuser or self.request.user == user): it = InstanceTemplate.get_objects_with_level("operator", self.request.user) context['instances_owned'] = context['instances_owned'].filter( template__in=it) context['instances_with_access'] = context[ 'instances_with_access'].filter(template__in=it) if self.request.user.is_superuser: context['login_token'] = TokenLogin.get_token_url( user, self.request.user) return context @require_POST def toggle_use_gravatar(request, **kwargs): user = get_object_or_404(User, username=kwargs['username']) if not request.user == user: raise PermissionDenied() profile = user.profile profile.use_gravatar = not profile.use_gravatar profile.save() new_avatar_url = user.profile.get_avatar_url() return HttpResponse( json.dumps({'new_avatar_url': new_avatar_url}), content_type="application/json", ) class UserKeyDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = UserKey template_name = "dashboard/userkey-edit.html" form_class = UserKeyForm success_message = _("Successfully modified SSH key.") def get(self, request, *args, **kwargs): object = self.get_object() if object.user != request.user: raise PermissionDenied() return super(UserKeyDetail, self).get(request, *args, **kwargs) def get_success_url(self): return reverse_lazy("dashboard.views.userkey-detail", kwargs=self.kwargs) def post(self, request, *args, **kwargs): object = self.get_object() if object.user != request.user: raise PermissionDenied() return super(UserKeyDetail, self).post(self, request, args, kwargs) class UserKeyDelete(LoginRequiredMixin, DeleteView): model = UserKey def get_success_url(self): return reverse("dashboard.views.profile-preferences") def get_template_names(self): if self.request.is_ajax(): return ['dashboard/confirm/ajax-delete.html'] else: return ['dashboard/confirm/base-delete.html'] def delete(self, request, *args, **kwargs): object = self.get_object() if object.user != request.user: raise PermissionDenied() object.delete() success_url = self.get_success_url() success_message = _("SSH key successfully deleted.") if request.is_ajax(): return HttpResponse( json.dumps({'message': success_message}), content_type="application/json", ) else: messages.success(request, success_message) return HttpResponseRedirect(success_url) class UserKeyCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView): model = UserKey form_class = UserKeyForm template_name = "dashboard/userkey-create.html" success_message = _("Successfully created a new SSH key.") def get_success_url(self): return reverse_lazy("dashboard.views.profile-preferences") def get_form_kwargs(self): kwargs = super(UserKeyCreate, self).get_form_kwargs() kwargs['user'] = self.request.user return kwargs class ConnectCommandDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): model = ConnectCommand template_name = "dashboard/connect-command-edit.html" form_class = ConnectCommandForm success_message = _("Successfully modified command template.") def get(self, request, *args, **kwargs): object = self.get_object() if object.user != request.user: raise PermissionDenied() return super(ConnectCommandDetail, self).get(request, *args, **kwargs) def get_success_url(self): return reverse_lazy("dashboard.views.connect-command-detail", kwargs=self.kwargs) def post(self, request, *args, **kwargs): object = self.get_object() if object.user != request.user: raise PermissionDenied() return super(ConnectCommandDetail, self).post(request, args, kwargs) def get_form_kwargs(self): kwargs = super(ConnectCommandDetail, self).get_form_kwargs() kwargs['user'] = self.request.user return kwargs class ConnectCommandDelete(LoginRequiredMixin, DeleteView): model = ConnectCommand def get_success_url(self): return reverse("dashboard.views.profile-preferences") def get_template_names(self): if self.request.is_ajax(): return ['dashboard/confirm/ajax-delete.html'] else: return ['dashboard/confirm/base-delete.html'] def delete(self, request, *args, **kwargs): object = self.get_object() if object.user != request.user: raise PermissionDenied() object.delete() success_url = self.get_success_url() success_message = _("Command template successfully deleted.") if request.is_ajax(): return HttpResponse( json.dumps({'message': success_message}), content_type="application/json", ) else: messages.success(request, success_message) return HttpResponseRedirect(success_url) class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView): model = ConnectCommand form_class = ConnectCommandForm template_name = "dashboard/connect-command-create.html" success_message = _("Successfully created a new command template.") def get_success_url(self): return reverse_lazy("dashboard.views.profile-preferences") def get_form_kwargs(self): kwargs = super(ConnectCommandCreate, self).get_form_kwargs() kwargs['user'] = self.request.user return kwargs