github.com/tmlbl/deis@v1.0.2/controller/api/views.py (about)

     1  """
     2  RESTful view classes for presenting Deis API objects.
     3  """
     4  
     5  from __future__ import absolute_import
     6  from __future__ import unicode_literals
     7  
     8  from django.conf import settings
     9  from django.contrib.auth.models import AnonymousUser, User
    10  from django.core.exceptions import ValidationError
    11  from django.http import Http404
    12  from django.utils import timezone
    13  from guardian.shortcuts import assign_perm
    14  from guardian.shortcuts import get_objects_for_user
    15  from guardian.shortcuts import get_users_with_perms
    16  from guardian.shortcuts import remove_perm
    17  from rest_framework import permissions
    18  from rest_framework import status
    19  from rest_framework import viewsets
    20  from rest_framework.authentication import BaseAuthentication
    21  from rest_framework.exceptions import PermissionDenied
    22  from rest_framework.generics import get_object_or_404
    23  from rest_framework.response import Response
    24  
    25  from api import models, serializers
    26  from api.permissions import IsAnonymous, IsOwner, IsAppUser, \
    27      IsAdmin, HasRegistrationAuth, HasBuilderAuth
    28  
    29  
    30  class AnonymousAuthentication(BaseAuthentication):
    31  
    32      def authenticate(self, request):
    33          """
    34          Authenticate the request and return a two-tuple of (user, token).
    35          """
    36          user = AnonymousUser()
    37          return user, None
    38  
    39  
    40  class UserRegistrationView(viewsets.GenericViewSet,
    41                             viewsets.mixins.CreateModelMixin):
    42      model = User
    43  
    44      authentication_classes = (AnonymousAuthentication,)
    45      permission_classes = (IsAnonymous, HasRegistrationAuth)
    46      serializer_class = serializers.UserSerializer
    47  
    48      def pre_save(self, obj):
    49          """Replicate UserManager.create_user functionality."""
    50          now = timezone.now()
    51          obj.last_login = now
    52          obj.date_joined = now
    53          obj.is_active = True
    54          obj.email = User.objects.normalize_email(obj.email)
    55          obj.set_password(obj.password)
    56          # Make this first signup an admin / superuser
    57          if not User.objects.filter(is_superuser=True).exists():
    58              obj.is_superuser = obj.is_staff = True
    59  
    60  
    61  class UserManagementView(viewsets.GenericViewSet,
    62                           viewsets.mixins.CreateModelMixin,
    63                           viewsets.mixins.DestroyModelMixin):
    64      model = User
    65      permission_classes = (permissions.IsAuthenticated,)
    66  
    67      def passwd(self, request, *args, **kwargs):
    68          obj = self.request.user
    69          if not obj.check_password(request.DATA['password']):
    70              return Response("Current password did not match", status=status.HTTP_400_BAD_REQUEST)
    71          obj.set_password(request.DATA['new_password'])
    72          obj.save()
    73          return Response({'status': 'password set'})
    74  
    75      def destroy(self, request, *args, **kwargs):
    76          obj = self.request.user
    77          obj.delete()
    78          return Response(status=status.HTTP_204_NO_CONTENT)
    79  
    80  
    81  class OwnerViewSet(viewsets.ModelViewSet):
    82      """Scope views to an `owner` attribute."""
    83  
    84      permission_classes = (permissions.IsAuthenticated, IsOwner)
    85  
    86      def pre_save(self, obj):
    87          obj.owner = self.request.user
    88  
    89      def get_queryset(self, **kwargs):
    90          """Filter all querysets by an `owner` attribute.
    91          """
    92          return self.model.objects.filter(owner=self.request.user)
    93  
    94  
    95  class AppPermsViewSet(viewsets.ViewSet):
    96      """RESTful views for sharing apps with collaborators."""
    97  
    98      model = models.App  # models class
    99      perm = 'use_app'    # short name for permission
   100  
   101      def list(self, request, **kwargs):
   102          app = get_object_or_404(self.model, id=kwargs['id'])
   103          perm_name = "api.{}".format(self.perm)
   104          if request.user != app.owner and \
   105                  not request.user.has_perm(perm_name, app) and \
   106                  not request.user.is_superuser:
   107              return Response(status=status.HTTP_403_FORBIDDEN)
   108          usernames = [u.username for u in get_users_with_perms(app)
   109                       if u.has_perm(perm_name, app)]
   110          return Response({'users': usernames})
   111  
   112      def create(self, request, **kwargs):
   113          app = get_object_or_404(self.model, id=kwargs['id'])
   114          if request.user != app.owner and not request.user.is_superuser:
   115              return Response(status=status.HTTP_403_FORBIDDEN)
   116          user = get_object_or_404(User, username=request.DATA['username'])
   117          assign_perm(self.perm, user, app)
   118          models.log_event(app, "User {} was granted access to {}".format(user, app))
   119          return Response(status=status.HTTP_201_CREATED)
   120  
   121      def destroy(self, request, **kwargs):
   122          app = get_object_or_404(self.model, id=kwargs['id'])
   123          if request.user != app.owner and not request.user.is_superuser:
   124              return Response(status=status.HTTP_403_FORBIDDEN)
   125          user = get_object_or_404(User, username=kwargs['username'])
   126          if user.has_perm(self.perm, app):
   127              remove_perm(self.perm, user, app)
   128              models.log_event(app, "User {} was revoked access to {}".format(user, app))
   129              return Response(status=status.HTTP_204_NO_CONTENT)
   130          else:
   131              return Response(status=status.HTTP_404_NOT_FOUND)
   132  
   133  
   134  class AdminPermsViewSet(viewsets.ModelViewSet):
   135      """RESTful views for sharing admin permissions with other users."""
   136  
   137      model = User
   138      serializer_class = serializers.AdminUserSerializer
   139      permission_classes = (IsAdmin,)
   140  
   141      def get_queryset(self, **kwargs):
   142          return self.model.objects.filter(is_active=True, is_superuser=True)
   143  
   144      def create(self, request, **kwargs):
   145          user = get_object_or_404(User, username=request.DATA['username'])
   146          user.is_superuser = user.is_staff = True
   147          user.save(update_fields=['is_superuser', 'is_staff'])
   148          return Response(status=status.HTTP_201_CREATED)
   149  
   150      def destroy(self, request, **kwargs):
   151          user = get_object_or_404(User, username=kwargs['username'])
   152          user.is_superuser = user.is_staff = False
   153          user.save(update_fields=['is_superuser', 'is_staff'])
   154          return Response(status=status.HTTP_204_NO_CONTENT)
   155  
   156  
   157  class AppViewSet(OwnerViewSet):
   158      """RESTful views for :class:`~api.models.App`."""
   159  
   160      model = models.App
   161      serializer_class = serializers.AppSerializer
   162      lookup_field = 'id'
   163      permission_classes = (permissions.IsAuthenticated, IsAppUser)
   164  
   165      def get_queryset(self, **kwargs):
   166          """
   167          Filter Apps by `owner` attribute or the `api.use_app` permission.
   168          """
   169          return super(AppViewSet, self).get_queryset(**kwargs) | \
   170              get_objects_for_user(self.request.user, 'api.use_app')
   171  
   172      def post_save(self, app, created=False, **kwargs):
   173          if created:
   174              app.create()
   175  
   176      def scale(self, request, **kwargs):
   177          new_structure = {}
   178          try:
   179              for target, count in request.DATA.items():
   180                  new_structure[target] = int(count)
   181          except (TypeError, ValueError):
   182              return Response('Invalid scaling format',
   183                              status=status.HTTP_400_BAD_REQUEST)
   184          app = self.get_object()
   185          try:
   186              models.validate_app_structure(new_structure)
   187              app.scale(request.user, new_structure)
   188          except (EnvironmentError, ValidationError) as e:
   189              return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
   190          except RuntimeError as e:
   191              return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE)
   192          return Response(status=status.HTTP_204_NO_CONTENT,
   193                          content_type='application/json')
   194  
   195      def logs(self, request, **kwargs):
   196          app = self.get_object()
   197          try:
   198              logs = app.logs()
   199          except EnvironmentError:
   200              return Response("No logs for {}".format(app.id),
   201                              status=status.HTTP_204_NO_CONTENT,
   202                              content_type='text/plain')
   203          return Response(logs, status=status.HTTP_200_OK,
   204                          content_type='text/plain')
   205  
   206      def run(self, request, **kwargs):
   207          app = self.get_object()
   208          command = request.DATA['command']
   209          try:
   210              output_and_rc = app.run(self.request.user, command)
   211          except EnvironmentError as e:
   212              return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
   213          except RuntimeError as e:
   214              return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE)
   215          return Response(output_and_rc, status=status.HTTP_200_OK,
   216                          content_type='text/plain')
   217  
   218      def destroy(self, request, **kwargs):
   219          obj = get_object_or_404(self.model, id=kwargs['id'])
   220          obj.delete()
   221          return Response(status=status.HTTP_204_NO_CONTENT)
   222  
   223  
   224  class BaseAppViewSet(viewsets.ModelViewSet):
   225  
   226      permission_classes = (permissions.IsAuthenticated, IsAppUser)
   227  
   228      def pre_save(self, obj):
   229          obj.owner = self.request.user
   230  
   231      def get_queryset(self, **kwargs):
   232          app = get_object_or_404(models.App, id=self.kwargs['id'])
   233          try:
   234              self.check_object_permissions(self.request, app)
   235          except PermissionDenied:
   236              raise Http404("No {} matches the given query.".format(
   237                  self.model._meta.object_name))
   238          return self.model.objects.filter(app=app)
   239  
   240      def get_object(self, *args, **kwargs):
   241          obj = self.get_queryset().latest('created')
   242          self.check_object_permissions(self.request, obj)
   243          return obj
   244  
   245  
   246  class AppBuildViewSet(BaseAppViewSet):
   247      """RESTful views for :class:`~api.models.Build`."""
   248  
   249      model = models.Build
   250      serializer_class = serializers.BuildSerializer
   251  
   252      def post_save(self, build, created=False):
   253          if created:
   254              self.release = build.create(self.request.user)
   255  
   256      def get_success_headers(self, data):
   257          headers = super(AppBuildViewSet, self).get_success_headers(data)
   258          headers.update({'X-Deis-Release': self.release.version})
   259          return headers
   260  
   261      def create(self, request, *args, **kwargs):
   262          app = get_object_or_404(models.App, id=self.kwargs['id'])
   263          request._data = request.DATA.copy()
   264          request.DATA['app'] = app
   265          try:
   266              return super(AppBuildViewSet, self).create(request, *args, **kwargs)
   267          except RuntimeError as e:
   268              return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE)
   269  
   270  
   271  class AppConfigViewSet(BaseAppViewSet):
   272      """RESTful views for :class:`~api.models.Config`."""
   273  
   274      model = models.Config
   275      serializer_class = serializers.ConfigSerializer
   276  
   277      def get_object(self, *args, **kwargs):
   278          """Return the Config associated with the App's latest Release."""
   279          app = get_object_or_404(models.App, id=self.kwargs['id'])
   280          try:
   281              self.check_object_permissions(self.request, app)
   282              return app.release_set.latest().config
   283          except (PermissionDenied, models.Release.DoesNotExist):
   284              raise Http404("No {} matches the given query.".format(
   285                  self.model._meta.object_name))
   286  
   287      def pre_save(self, config):
   288          """merge the old config with the new"""
   289          previous_config = config.app.config_set.latest()
   290          config.owner = self.request.user
   291          if previous_config:
   292              config.owner = previous_config.owner
   293              for attr in ['cpu', 'memory', 'tags', 'values']:
   294                  # Guard against migrations from older apps without fixes to
   295                  # JSONField encoding.
   296                  try:
   297                      data = getattr(previous_config, attr).copy()
   298                  except AttributeError:
   299                      data = {}
   300                  try:
   301                      new_data = getattr(config, attr).copy()
   302                  except AttributeError:
   303                      new_data = {}
   304                  data.update(new_data)
   305                  # remove config keys if we provided a null value
   306                  [data.pop(k) for k, v in new_data.items() if v is None]
   307                  setattr(config, attr, data)
   308  
   309      def post_save(self, config, created=False):
   310          if created:
   311              release = config.app.release_set.latest()
   312              self.release = release.new(self.request.user, config=config, build=release.build)
   313              try:
   314                  config.app.deploy(self.request.user, self.release)
   315              except RuntimeError:
   316                  self.release.delete()
   317                  raise
   318  
   319      def get_success_headers(self, data):
   320          headers = super(AppConfigViewSet, self).get_success_headers(data)
   321          headers.update({'X-Deis-Release': self.release.version})
   322          return headers
   323  
   324      def create(self, request, *args, **kwargs):
   325          obj = self.get_object()
   326          request.DATA['app'] = obj.app
   327          try:
   328              return super(AppConfigViewSet, self).create(request, *args, **kwargs)
   329          except RuntimeError as e:
   330              return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE)
   331  
   332  
   333  class AppReleaseViewSet(BaseAppViewSet):
   334      """RESTful views for :class:`~api.models.Release`."""
   335  
   336      model = models.Release
   337      serializer_class = serializers.ReleaseSerializer
   338  
   339      def get_object(self, *args, **kwargs):
   340          """Get Release by version always."""
   341          return self.get_queryset(**kwargs).get(version=self.kwargs['version'])
   342  
   343      def rollback(self, request, *args, **kwargs):
   344          """
   345          Create a new release as a copy of the state of the compiled slug and
   346          config vars of a previous release.
   347          """
   348          try:
   349              app = get_object_or_404(models.App, id=self.kwargs['id'])
   350              release = app.release_set.latest()
   351              version_to_rollback_to = release.version - 1
   352              if request.DATA.get('version'):
   353                  version_to_rollback_to = int(request.DATA['version'])
   354              new_release = release.rollback(request.user, version_to_rollback_to)
   355              response = {'version': new_release.version}
   356              return Response(response, status=status.HTTP_201_CREATED)
   357          except EnvironmentError as e:
   358              return Response(str(e), status=status.HTTP_400_BAD_REQUEST)
   359          except RuntimeError as e:
   360              new_release.delete()
   361              return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE)
   362  
   363  
   364  class AppContainerViewSet(BaseAppViewSet):
   365      """RESTful views for :class:`~api.models.Container`."""
   366  
   367      model = models.Container
   368      serializer_class = serializers.ContainerSerializer
   369  
   370      def get_queryset(self, **kwargs):
   371          qs = super(AppContainerViewSet, self).get_queryset(**kwargs)
   372          container_type = self.kwargs.get('type')
   373          if container_type:
   374              qs = qs.filter(type=container_type)
   375          else:
   376              qs = qs.exclude(type='run')
   377          return qs
   378  
   379      def get_object(self, *args, **kwargs):
   380          qs = self.get_queryset(**kwargs)
   381          obj = qs.get(num=self.kwargs['num'])
   382          return obj
   383  
   384  
   385  class KeyViewSet(OwnerViewSet):
   386      """RESTful views for :class:`~api.models.Key`."""
   387  
   388      model = models.Key
   389      serializer_class = serializers.KeySerializer
   390      lookup_field = 'id'
   391  
   392  
   393  class DomainViewSet(OwnerViewSet):
   394      """RESTful views for :class:`~api.models.Domain`."""
   395  
   396      model = models.Domain
   397      serializer_class = serializers.DomainSerializer
   398  
   399      def create(self, request, *args, **kwargs):
   400          app = get_object_or_404(models.App, id=self.kwargs['id'])
   401          request._data = request.DATA.copy()
   402          request.DATA['app'] = app
   403          return super(DomainViewSet, self).create(request, *args, **kwargs)
   404  
   405      def get_queryset(self, **kwargs):
   406          app = get_object_or_404(models.App, id=self.kwargs['id'])
   407          qs = self.model.objects.filter(app=app)
   408          return qs
   409  
   410      def get_object(self, *args, **kwargs):
   411          qs = self.get_queryset(**kwargs)
   412          obj = qs.get(domain=self.kwargs['domain'])
   413          return obj
   414  
   415  
   416  class BaseHookViewSet(viewsets.ModelViewSet):
   417  
   418      permission_classes = (HasBuilderAuth,)
   419  
   420      def pre_save(self, obj):
   421          # SECURITY: we trust the username field to map to the owner
   422          obj.owner = self.request.DATA['owner']
   423  
   424  
   425  class PushHookViewSet(BaseHookViewSet):
   426      """API hook to create new :class:`~api.models.Push`"""
   427  
   428      model = models.Push
   429      serializer_class = serializers.PushSerializer
   430  
   431      def create(self, request, *args, **kwargs):
   432          app = get_object_or_404(models.App, id=request.DATA['receive_repo'])
   433          user = get_object_or_404(
   434              User, username=request.DATA['receive_user'])
   435          # check the user is authorized for this app
   436          if user == app.owner or \
   437             user in get_users_with_perms(app) or \
   438             user.is_superuser:
   439              request._data = request.DATA.copy()
   440              request.DATA['app'] = app
   441              request.DATA['owner'] = user
   442              return super(PushHookViewSet, self).create(request, *args, **kwargs)
   443          raise PermissionDenied()
   444  
   445  
   446  class BuildHookViewSet(BaseHookViewSet):
   447      """API hook to create new :class:`~api.models.Build`"""
   448  
   449      model = models.Build
   450      serializer_class = serializers.BuildSerializer
   451  
   452      def create(self, request, *args, **kwargs):
   453          app = get_object_or_404(models.App, id=request.DATA['receive_repo'])
   454          self.user = get_object_or_404(
   455              User, username=request.DATA['receive_user'])
   456          # check the user is authorized for this app
   457          if self.user == app.owner or \
   458             self.user in get_users_with_perms(app) or \
   459             self.user.is_superuser:
   460              request._data = request.DATA.copy()
   461              request.DATA['app'] = app
   462              request.DATA['owner'] = self.user
   463              try:
   464                  super(BuildHookViewSet, self).create(request, *args, **kwargs)
   465                  # return the application databag
   466                  response = {'release': {'version': app.release_set.latest().version},
   467                              'domains': ['.'.join([app.id, settings.DEIS_DOMAIN])]}
   468                  return Response(response, status=status.HTTP_200_OK)
   469              except RuntimeError as e:
   470                  return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE)
   471          raise PermissionDenied()
   472  
   473      def post_save(self, build, created=False):
   474          if created:
   475              build.create(self.user)
   476  
   477  
   478  class ConfigHookViewSet(BaseHookViewSet):
   479      """API hook to grab latest :class:`~api.models.Config`"""
   480  
   481      model = models.Config
   482      serializer_class = serializers.ConfigSerializer
   483  
   484      def create(self, request, *args, **kwargs):
   485          app = get_object_or_404(models.App, id=request.DATA['receive_repo'])
   486          user = get_object_or_404(
   487              User, username=request.DATA['receive_user'])
   488          # check the user is authorized for this app
   489          if user == app.owner or \
   490             user in get_users_with_perms(app) or \
   491             user.is_superuser:
   492              config = app.release_set.latest().config
   493              serializer = self.get_serializer(config)
   494              return Response(serializer.data, status=status.HTTP_200_OK)
   495          raise PermissionDenied()