github.com/dustinrc/deis@v1.10.1-0.20150917223407-0894a5fb979e/controller/api/views.py (about)

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