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