github.com/chasestarr/deis@v1.13.5-0.20170519182049-1d9e59fbdbfc/controller/api/views.py (about)

     1  """
     2  RESTful view classes for presenting Deis API objects.
     3  """
     4  
     5  import os.path
     6  import tempfile
     7  
     8  from django.conf import settings
     9  from django.core.exceptions import ValidationError
    10  from django.contrib.auth.models import User
    11  from django.http import HttpResponse
    12  from django.shortcuts import get_object_or_404
    13  from guardian.shortcuts import assign_perm, get_objects_for_user, \
    14      get_users_with_perms, remove_perm
    15  from django.views.generic import View
    16  from rest_framework import mixins, renderers, status
    17  from rest_framework.exceptions import PermissionDenied
    18  from rest_framework.permissions import IsAuthenticated
    19  from rest_framework.response import Response
    20  from rest_framework.viewsets import GenericViewSet
    21  from rest_framework.authtoken.models import Token
    22  from simpleflock import SimpleFlock
    23  
    24  from api import authentication, models, permissions, serializers, viewsets
    25  
    26  import requests
    27  
    28  
    29  class HealthCheckView(View):
    30      """Simple health check view to determine if the server
    31         is responding to HTTP requests.
    32      """
    33  
    34      def get(self, request):
    35          return HttpResponse("OK")
    36      head = get
    37  
    38  
    39  class UserRegistrationViewSet(GenericViewSet,
    40                                mixins.CreateModelMixin):
    41      """ViewSet to handle registering new users. The logic is in the serializer."""
    42      authentication_classes = [authentication.AnonymousOrAuthenticatedAuthentication]
    43      permission_classes = [permissions.HasRegistrationAuth]
    44      serializer_class = serializers.UserSerializer
    45  
    46  
    47  class UserManagementViewSet(GenericViewSet):
    48      serializer_class = serializers.UserSerializer
    49  
    50      def get_queryset(self):
    51          return User.objects.filter(pk=self.request.user.pk)
    52  
    53      def get_object(self):
    54          return self.get_queryset()[0]
    55  
    56      def destroy(self, request, **kwargs):
    57          calling_obj = self.get_object()
    58          target_obj = calling_obj
    59  
    60          if request.data.get('username'):
    61              # if you "accidentally" target yourself, that should be fine
    62              if calling_obj.username == request.data['username'] or calling_obj.is_superuser:
    63                  target_obj = get_object_or_404(User, username=request.data['username'])
    64              else:
    65                  raise PermissionDenied()
    66  
    67          # A user can not be removed without apps changing ownership first
    68          if len(models.App.objects.filter(owner=target_obj)) > 0:
    69              msg = '{} still has applications assigned. Delete or transfer ownership'.format(str(target_obj))  # noqa
    70              return Response({'detail': msg}, status=status.HTTP_409_CONFLICT)
    71  
    72          target_obj.delete()
    73          return Response(status=status.HTTP_204_NO_CONTENT)
    74  
    75      def passwd(self, request, **kwargs):
    76          caller_obj = self.get_object()
    77          target_obj = self.get_object()
    78          if request.data.get('username'):
    79              # if you "accidentally" target yourself, that should be fine
    80              if caller_obj.username == request.data['username'] or caller_obj.is_superuser:
    81                  target_obj = get_object_or_404(User, username=request.data['username'])
    82              else:
    83                  raise PermissionDenied()
    84          if request.data.get('password') or not caller_obj.is_superuser:
    85              if not target_obj.check_password(request.data['password']):
    86                  return Response({'detail': 'Current password does not match'},
    87                                  status=status.HTTP_400_BAD_REQUEST)
    88          target_obj.set_password(request.data['new_password'])
    89          target_obj.save()
    90          return Response({'status': 'password set'})
    91  
    92  
    93  class TokenManagementViewSet(GenericViewSet,
    94                               mixins.DestroyModelMixin):
    95      serializer_class = serializers.UserSerializer
    96      permission_classes = [permissions.CanRegenerateToken]
    97  
    98      def get_queryset(self):
    99          return User.objects.filter(pk=self.request.user.pk)
   100  
   101      def get_object(self):
   102          return self.get_queryset()[0]
   103  
   104      def regenerate(self, request, **kwargs):
   105          obj = self.get_object()
   106  
   107          if 'all' in request.data:
   108              for user in User.objects.all():
   109                  if not user.is_anonymous():
   110                      token = Token.objects.get(user=user)
   111                      token.delete()
   112                      Token.objects.create(user=user)
   113              return Response("")
   114  
   115          if 'username' in request.data:
   116              obj = get_object_or_404(User,
   117                                      username=request.data['username'])
   118              self.check_object_permissions(self.request, obj)
   119  
   120          token = Token.objects.get(user=obj)
   121          token.delete()
   122          token = Token.objects.create(user=obj)
   123          return Response({'token': token.key})
   124  
   125  
   126  class BaseDeisViewSet(viewsets.OwnerViewSet):
   127      """
   128      A generic ViewSet for objects related to Deis.
   129  
   130      To use it, at minimum you'll need to provide the `serializer_class` attribute and
   131      the `model` attribute shortcut.
   132      """
   133      lookup_field = 'id'
   134      permission_classes = [IsAuthenticated, permissions.IsAppUser]
   135      renderer_classes = [renderers.JSONRenderer]
   136  
   137      def create(self, request, *args, **kwargs):
   138          try:
   139              return super(BaseDeisViewSet, self).create(request, *args, **kwargs)
   140          # If the scheduler oopsie'd
   141          except RuntimeError as e:
   142              return Response({'detail': str(e)}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
   143  
   144  
   145  class AppResourceViewSet(BaseDeisViewSet):
   146      """A viewset for objects which are attached to an application."""
   147  
   148      def get_app(self):
   149          app = get_object_or_404(models.App, id=self.kwargs['id'])
   150          self.check_object_permissions(self.request, app)
   151          return app
   152  
   153      def get_queryset(self, **kwargs):
   154          app = self.get_app()
   155          return self.model.objects.filter(app=app)
   156  
   157      def get_object(self, **kwargs):
   158          return self.get_queryset(**kwargs).latest('created')
   159  
   160      def create(self, request, **kwargs):
   161          request.data['app'] = self.get_app()
   162          return super(AppResourceViewSet, self).create(request, **kwargs)
   163  
   164  
   165  class ReleasableViewSet(AppResourceViewSet):
   166      """A viewset for application resources which affect the release cycle.
   167  
   168      When a resource is created, a new release is created for the application
   169      and it returns some success headers regarding the new release.
   170  
   171      To use it, at minimum you'll need to provide a `release` attribute tied to your class before
   172      calling post_save().
   173      """
   174      def get_object(self):
   175          """Retrieve the object based on the latest release's value"""
   176          return getattr(self.get_app().release_set.latest(), self.model.__name__.lower())
   177  
   178      def get_success_headers(self, data, **kwargs):
   179          headers = super(ReleasableViewSet, self).get_success_headers(data)
   180          headers.update({'Deis-Release': self.release.version})
   181          headers.update({'X-Deis-Release': self.release.version})  # DEPRECATED
   182          return headers
   183  
   184  
   185  class AppViewSet(BaseDeisViewSet):
   186      """A viewset for interacting with App objects."""
   187      model = models.App
   188      serializer_class = serializers.AppSerializer
   189  
   190      def get_queryset(self, *args, **kwargs):
   191          return self.model.objects.all(*args, **kwargs)
   192  
   193      def list(self, request, *args, **kwargs):
   194          """
   195          HACK: Instead of filtering by the queryset, we limit the queryset to list only the apps
   196          which are owned by the user as well as any apps they have been given permission to
   197          interact with.
   198          """
   199          queryset = super(AppViewSet, self).get_queryset(**kwargs) | \
   200              get_objects_for_user(self.request.user, 'api.use_app')
   201          instance = self.filter_queryset(queryset)
   202          page = self.paginate_queryset(instance)
   203          if page is not None:
   204              serializer = self.get_pagination_serializer(page)
   205          else:
   206              serializer = self.get_serializer(instance, many=True)
   207          return Response(serializer.data)
   208  
   209      def post_save(self, app):
   210          app.create()
   211  
   212      def scale(self, request, **kwargs):
   213          new_structure = {}
   214          app = self.get_object()
   215          try:
   216              for target, count in request.data.viewitems():
   217                  new_structure[target] = int(count)
   218              models.validate_app_structure(new_structure)
   219              app.scale(request.user, new_structure)
   220          except (TypeError, ValueError) as e:
   221              return Response({'detail': 'Invalid scaling format: {}'.format(e)},
   222                              status=status.HTTP_400_BAD_REQUEST)
   223          except (EnvironmentError, ValidationError) as e:
   224              return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
   225          except RuntimeError as e:
   226              return Response({'detail': str(e)}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
   227          return Response(status=status.HTTP_204_NO_CONTENT)
   228  
   229      def logs(self, request, **kwargs):
   230          app = self.get_object()
   231          try:
   232              return HttpResponse(app.logs(request.query_params.get('log_lines',
   233                                           str(settings.LOG_LINES))),
   234                                  status=status.HTTP_200_OK, content_type='text/plain')
   235          except requests.exceptions.RequestException:
   236              return HttpResponse("Error accessing logs for {}".format(app.id),
   237                                  status=status.HTTP_500_INTERNAL_SERVER_ERROR,
   238                                  content_type='text/plain')
   239          except EnvironmentError as e:
   240              if e.message == 'Error accessing deis-logger':
   241                  return HttpResponse("Error accessing logs for {}".format(app.id),
   242                                      status=status.HTTP_500_INTERNAL_SERVER_ERROR,
   243                                      content_type='text/plain')
   244              else:
   245                  return HttpResponse(status=status.HTTP_204_NO_CONTENT)
   246  
   247      def run(self, request, **kwargs):
   248          app = self.get_object()
   249          try:
   250              output_and_rc = app.run(self.request.user, request.data['command'])
   251          except EnvironmentError as e:
   252              return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
   253          except RuntimeError as e:
   254              return Response({'detail': str(e)}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
   255          return Response(output_and_rc, status=status.HTTP_200_OK,
   256                          content_type='text/plain')
   257  
   258      def update(self, request, **kwargs):
   259          app = self.get_object()
   260  
   261          if request.data.get('owner'):
   262              if self.request.user != app.owner and not self.request.user.is_superuser:
   263                  raise PermissionDenied()
   264              new_owner = get_object_or_404(User, username=request.data['owner'])
   265              app.owner = new_owner
   266          app.save()
   267          return Response(status=status.HTTP_200_OK)
   268  
   269  
   270  class BuildViewSet(ReleasableViewSet):
   271      """A viewset for interacting with Build objects."""
   272      model = models.Build
   273      serializer_class = serializers.BuildSerializer
   274  
   275      def post_save(self, build):
   276          self.release = build.create(self.request.user)
   277          super(BuildViewSet, self).post_save(build)
   278  
   279  
   280  class ConfigViewSet(ReleasableViewSet):
   281      """A viewset for interacting with Config objects."""
   282      model = models.Config
   283      serializer_class = serializers.ConfigSerializer
   284  
   285      def create(self, request, **kwargs):
   286          # Guard against overlapping config changes, using a filesystem lock so that
   287          # multiple controller processes can be coordinated.
   288          # Use a tempfile such as "/tmp/violet-valkyrie-config".
   289          lockfile = os.path.join(tempfile.gettempdir(), kwargs['id'] + '-config')
   290          try:
   291              with SimpleFlock(lockfile, timeout=5):
   292                  return super(ConfigViewSet, self).create(request, **kwargs)
   293          except IOError as err:
   294              msg = "Config changes already in progress.\n{}".format(err)
   295              return Response(status=status.HTTP_409_CONFLICT, data={'error': msg})
   296  
   297      def post_save(self, config):
   298          release = config.app.release_set.latest()
   299          self.release = release.new(self.request.user, config=config, build=release.build)
   300          try:
   301              config.app.deploy(self.request.user, self.release)
   302          except RuntimeError:
   303              self.release.delete()
   304              raise
   305  
   306  
   307  class ContainerViewSet(AppResourceViewSet):
   308      """A viewset for interacting with Container objects."""
   309      model = models.Container
   310      serializer_class = serializers.ContainerSerializer
   311  
   312      def get_queryset(self, **kwargs):
   313          qs = super(ContainerViewSet, self).get_queryset(**kwargs)
   314          container_type = self.kwargs.get('type')
   315          if container_type:
   316              qs = qs.filter(type=container_type)
   317          else:
   318              qs = qs.exclude(type='run')
   319          return qs
   320  
   321      def get_object(self, **kwargs):
   322          qs = self.get_queryset(**kwargs)
   323          return qs.get(num=self.kwargs['num'])
   324  
   325      def restart(self, *args, **kwargs):
   326          try:
   327              containers = self.get_app().restart(**kwargs)
   328              serializer = self.get_serializer(containers, many=True)
   329              return Response(serializer.data, status=status.HTTP_200_OK)
   330          except Exception as e:
   331              return Response({'detail': str(e)}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
   332  
   333  
   334  class DomainViewSet(AppResourceViewSet):
   335      """A viewset for interacting with Domain objects."""
   336      model = models.Domain
   337      serializer_class = serializers.DomainSerializer
   338  
   339      def get_object(self, **kwargs):
   340          qs = self.get_queryset(**kwargs)
   341          return qs.get(domain=self.kwargs['domain'])
   342  
   343  
   344  class CertificateViewSet(BaseDeisViewSet):
   345      """A viewset for interacting with Domain objects."""
   346      model = models.Certificate
   347      serializer_class = serializers.CertificateSerializer
   348  
   349      def get_object(self, **kwargs):
   350          """Retrieve domain certificate by common name"""
   351          qs = self.get_queryset(**kwargs)
   352          return qs.get(common_name=self.kwargs['common_name'])
   353  
   354  
   355  class KeyViewSet(BaseDeisViewSet):
   356      """A viewset for interacting with Key objects."""
   357      model = models.Key
   358      permission_classes = [IsAuthenticated, permissions.IsOwner]
   359      serializer_class = serializers.KeySerializer
   360  
   361  
   362  class ReleaseViewSet(AppResourceViewSet):
   363      """A viewset for interacting with Release objects."""
   364      model = models.Release
   365      serializer_class = serializers.ReleaseSerializer
   366  
   367      def get_object(self, **kwargs):
   368          """Get release by version always"""
   369          return self.get_queryset(**kwargs).get(version=self.kwargs['version'])
   370  
   371      def rollback(self, request, **kwargs):
   372          """
   373          Create a new release as a copy of the state of the compiled slug and config vars of a
   374          previous release.
   375          """
   376          app = self.get_app()
   377          try:
   378              release = app.release_set.latest()
   379              version_to_rollback_to = release.version - 1
   380              if request.data.get('version'):
   381                  version_to_rollback_to = int(request.data['version'])
   382              new_release = release.rollback(request.user, version_to_rollback_to)
   383              response = {'version': new_release.version}
   384              return Response(response, status=status.HTTP_201_CREATED)
   385          except EnvironmentError as e:
   386              return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
   387          except RuntimeError:
   388              new_release.delete()
   389              raise
   390  
   391  
   392  class BaseHookViewSet(BaseDeisViewSet):
   393      permission_classes = [permissions.HasBuilderAuth]
   394  
   395  
   396  class PushHookViewSet(BaseHookViewSet):
   397      """API hook to create new :class:`~api.models.Push`"""
   398      model = models.Push
   399      serializer_class = serializers.PushSerializer
   400  
   401      def create(self, request, *args, **kwargs):
   402          app = get_object_or_404(models.App, id=request.data['receive_repo'])
   403          request.user = get_object_or_404(User, username=request.data['receive_user'])
   404          # check the user is authorized for this app
   405          if not permissions.is_app_user(request, app):
   406              raise PermissionDenied()
   407          request.data['app'] = app
   408          request.data['owner'] = request.user
   409          return super(PushHookViewSet, self).create(request, *args, **kwargs)
   410  
   411  
   412  class BuildHookViewSet(BaseHookViewSet):
   413      """API hook to create new :class:`~api.models.Build`"""
   414      model = models.Build
   415      serializer_class = serializers.BuildSerializer
   416  
   417      def create(self, request, *args, **kwargs):
   418          app = get_object_or_404(models.App, id=request.data['receive_repo'])
   419          self.user = request.user = get_object_or_404(User, username=request.data['receive_user'])
   420          # check the user is authorized for this app
   421          if not permissions.is_app_user(request, app):
   422              raise PermissionDenied()
   423          request.data['app'] = app
   424          request.data['owner'] = self.user
   425          super(BuildHookViewSet, self).create(request, *args, **kwargs)
   426          # return the application databag
   427          response = {'release': {'version': app.release_set.latest().version},
   428                      'domains': ['.'.join([app.id, settings.DEIS_DOMAIN])]}
   429          return Response(response, status=status.HTTP_200_OK)
   430  
   431      def post_save(self, build):
   432          build.create(self.user)
   433  
   434  
   435  class ConfigHookViewSet(BaseHookViewSet):
   436      """API hook to grab latest :class:`~api.models.Config`"""
   437      model = models.Config
   438      serializer_class = serializers.ConfigSerializer
   439  
   440      def create(self, request, *args, **kwargs):
   441          app = get_object_or_404(models.App, id=request.data['receive_repo'])
   442          request.user = get_object_or_404(User, username=request.data['receive_user'])
   443          # check the user is authorized for this app
   444          if not permissions.is_app_user(request, app):
   445              raise PermissionDenied()
   446          config = app.release_set.latest().config
   447          serializer = self.get_serializer(config)
   448          return Response(serializer.data, status=status.HTTP_200_OK)
   449  
   450  
   451  class AppPermsViewSet(BaseDeisViewSet):
   452      """RESTful views for sharing apps with collaborators."""
   453  
   454      model = models.App  # models class
   455      perm = 'use_app'    # short name for permission
   456  
   457      def get_queryset(self):
   458          return self.model.objects.all()
   459  
   460      def list(self, request, **kwargs):
   461          app = self.get_object()
   462          perm_name = "api.{}".format(self.perm)
   463          usernames = [u.username for u in get_users_with_perms(app)
   464                       if u.has_perm(perm_name, app)]
   465          return Response({'users': usernames})
   466  
   467      def create(self, request, **kwargs):
   468          app = self.get_object()
   469          if not permissions.IsOwnerOrAdmin.has_object_permission(permissions.IsOwnerOrAdmin(),
   470                                                                  request, self, app):
   471              raise PermissionDenied()
   472  
   473          user = get_object_or_404(User, username=request.data['username'])
   474          assign_perm(self.perm, user, app)
   475          models.log_event(app, "User {} was granted access to {}".format(user, app))
   476          return Response(status=status.HTTP_201_CREATED)
   477  
   478      def destroy(self, request, **kwargs):
   479          app = get_object_or_404(models.App, id=self.kwargs['id'])
   480          user = get_object_or_404(User, username=kwargs['username'])
   481  
   482          perm_name = "api.{}".format(self.perm)
   483          if not user.has_perm(perm_name, app):
   484              raise PermissionDenied()
   485  
   486          if (user != request.user and
   487              not permissions.IsOwnerOrAdmin.has_object_permission(permissions.IsOwnerOrAdmin(),
   488                                                                   request, self, app)):
   489              raise PermissionDenied()
   490          remove_perm(self.perm, user, app)
   491          models.log_event(app, "User {} was revoked access to {}".format(user, app))
   492          return Response(status=status.HTTP_204_NO_CONTENT)
   493  
   494  
   495  class AdminPermsViewSet(BaseDeisViewSet):
   496      """RESTful views for sharing admin permissions with other users."""
   497  
   498      model = User
   499      serializer_class = serializers.AdminUserSerializer
   500      permission_classes = [permissions.IsAdmin]
   501  
   502      def get_queryset(self, **kwargs):
   503          self.check_object_permissions(self.request, self.request.user)
   504          return self.model.objects.filter(is_active=True, is_superuser=True)
   505  
   506      def create(self, request, **kwargs):
   507          user = get_object_or_404(User, username=request.data['username'])
   508          user.is_superuser = user.is_staff = True
   509          user.save(update_fields=['is_superuser', 'is_staff'])
   510          return Response(status=status.HTTP_201_CREATED)
   511  
   512      def destroy(self, request, **kwargs):
   513          user = get_object_or_404(User, username=kwargs['username'])
   514          user.is_superuser = user.is_staff = False
   515          user.save(update_fields=['is_superuser', 'is_staff'])
   516          return Response(status=status.HTTP_204_NO_CONTENT)
   517  
   518  
   519  class UserView(BaseDeisViewSet):
   520      """A Viewset for interacting with User objects."""
   521      model = User
   522      serializer_class = serializers.UserSerializer
   523      permission_classes = [permissions.IsAdmin]
   524  
   525      def get_queryset(self):
   526          return self.model.objects.exclude(username='AnonymousUser')