github.com/rochacon/deis@v1.0.2-0.20150903015341-6839b592a1ff/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  
   228  class BuildViewSet(ReleasableViewSet):
   229      """A viewset for interacting with Build objects."""
   230      model = models.Build
   231      serializer_class = serializers.BuildSerializer
   232  
   233      def post_save(self, build):
   234          self.release = build.create(self.request.user)
   235          super(BuildViewSet, self).post_save(build)
   236  
   237  
   238  class ConfigViewSet(ReleasableViewSet):
   239      """A viewset for interacting with Config objects."""
   240      model = models.Config
   241      serializer_class = serializers.ConfigSerializer
   242  
   243      def post_save(self, config):
   244          release = config.app.release_set.latest()
   245          self.release = release.new(self.request.user, config=config, build=release.build)
   246          try:
   247              config.app.deploy(self.request.user, self.release)
   248          except RuntimeError:
   249              self.release.delete()
   250              raise
   251  
   252  
   253  class ContainerViewSet(AppResourceViewSet):
   254      """A viewset for interacting with Container objects."""
   255      model = models.Container
   256      serializer_class = serializers.ContainerSerializer
   257  
   258      def get_queryset(self, **kwargs):
   259          qs = super(ContainerViewSet, self).get_queryset(**kwargs)
   260          container_type = self.kwargs.get('type')
   261          if container_type:
   262              qs = qs.filter(type=container_type)
   263          else:
   264              qs = qs.exclude(type='run')
   265          return qs
   266  
   267      def get_object(self, **kwargs):
   268          qs = self.get_queryset(**kwargs)
   269          return qs.get(num=self.kwargs['num'])
   270  
   271      def restart(self, *args, **kwargs):
   272          try:
   273              containers = self.get_app().restart(**kwargs)
   274              serializer = self.get_serializer(containers, many=True)
   275              return Response(serializer.data, status=status.HTTP_200_OK)
   276          except Exception as e:
   277              return Response({'detail': str(e)}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
   278  
   279  
   280  class DomainViewSet(AppResourceViewSet):
   281      """A viewset for interacting with Domain objects."""
   282      model = models.Domain
   283      serializer_class = serializers.DomainSerializer
   284  
   285      def get_object(self, **kwargs):
   286          qs = self.get_queryset(**kwargs)
   287          return qs.get(domain=self.kwargs['domain'])
   288  
   289  
   290  class CertificateViewSet(BaseDeisViewSet):
   291      """A viewset for interacting with Domain objects."""
   292      model = models.Certificate
   293      serializer_class = serializers.CertificateSerializer
   294  
   295      def get_object(self, **kwargs):
   296          """Retrieve domain certificate by common name"""
   297          qs = self.get_queryset(**kwargs)
   298          return qs.get(common_name=self.kwargs['common_name'])
   299  
   300  
   301  class KeyViewSet(BaseDeisViewSet):
   302      """A viewset for interacting with Key objects."""
   303      model = models.Key
   304      permission_classes = [IsAuthenticated, permissions.IsOwner]
   305      serializer_class = serializers.KeySerializer
   306  
   307  
   308  class ReleaseViewSet(AppResourceViewSet):
   309      """A viewset for interacting with Release objects."""
   310      model = models.Release
   311      serializer_class = serializers.ReleaseSerializer
   312  
   313      def get_object(self, **kwargs):
   314          """Get release by version always"""
   315          return self.get_queryset(**kwargs).get(version=self.kwargs['version'])
   316  
   317      def rollback(self, request, **kwargs):
   318          """
   319          Create a new release as a copy of the state of the compiled slug and config vars of a
   320          previous release.
   321          """
   322          app = self.get_app()
   323          try:
   324              release = app.release_set.latest()
   325              version_to_rollback_to = release.version - 1
   326              if request.data.get('version'):
   327                  version_to_rollback_to = int(request.data['version'])
   328              new_release = release.rollback(request.user, version_to_rollback_to)
   329              response = {'version': new_release.version}
   330              return Response(response, status=status.HTTP_201_CREATED)
   331          except EnvironmentError as e:
   332              return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
   333          except RuntimeError:
   334              new_release.delete()
   335              raise
   336  
   337  
   338  class BaseHookViewSet(BaseDeisViewSet):
   339      permission_classes = [permissions.HasBuilderAuth]
   340  
   341  
   342  class PushHookViewSet(BaseHookViewSet):
   343      """API hook to create new :class:`~api.models.Push`"""
   344      model = models.Push
   345      serializer_class = serializers.PushSerializer
   346  
   347      def create(self, request, *args, **kwargs):
   348          app = get_object_or_404(models.App, id=request.data['receive_repo'])
   349          request.user = get_object_or_404(User, username=request.data['receive_user'])
   350          # check the user is authorized for this app
   351          if not permissions.is_app_user(request, app):
   352              raise PermissionDenied()
   353          request.data['app'] = app
   354          request.data['owner'] = request.user
   355          return super(PushHookViewSet, self).create(request, *args, **kwargs)
   356  
   357  
   358  class BuildHookViewSet(BaseHookViewSet):
   359      """API hook to create new :class:`~api.models.Build`"""
   360      model = models.Build
   361      serializer_class = serializers.BuildSerializer
   362  
   363      def create(self, request, *args, **kwargs):
   364          app = get_object_or_404(models.App, id=request.data['receive_repo'])
   365          self.user = request.user = get_object_or_404(User, username=request.data['receive_user'])
   366          # check the user is authorized for this app
   367          if not permissions.is_app_user(request, app):
   368              raise PermissionDenied()
   369          request.data['app'] = app
   370          request.data['owner'] = self.user
   371          super(BuildHookViewSet, self).create(request, *args, **kwargs)
   372          # return the application databag
   373          response = {'release': {'version': app.release_set.latest().version},
   374                      'domains': ['.'.join([app.id, settings.DEIS_DOMAIN])]}
   375          return Response(response, status=status.HTTP_200_OK)
   376  
   377      def post_save(self, build):
   378          build.create(self.user)
   379  
   380  
   381  class ConfigHookViewSet(BaseHookViewSet):
   382      """API hook to grab latest :class:`~api.models.Config`"""
   383      model = models.Config
   384      serializer_class = serializers.ConfigSerializer
   385  
   386      def create(self, request, *args, **kwargs):
   387          app = get_object_or_404(models.App, id=request.data['receive_repo'])
   388          request.user = get_object_or_404(User, username=request.data['receive_user'])
   389          # check the user is authorized for this app
   390          if not permissions.is_app_user(request, app):
   391              raise PermissionDenied()
   392          config = app.release_set.latest().config
   393          serializer = self.get_serializer(config)
   394          return Response(serializer.data, status=status.HTTP_200_OK)
   395  
   396  
   397  class AppPermsViewSet(BaseDeisViewSet):
   398      """RESTful views for sharing apps with collaborators."""
   399  
   400      model = models.App  # models class
   401      perm = 'use_app'    # short name for permission
   402  
   403      def get_queryset(self):
   404          return self.model.objects.all()
   405  
   406      def list(self, request, **kwargs):
   407          app = self.get_object()
   408          perm_name = "api.{}".format(self.perm)
   409          usernames = [u.username for u in get_users_with_perms(app)
   410                       if u.has_perm(perm_name, app)]
   411          return Response({'users': usernames})
   412  
   413      def create(self, request, **kwargs):
   414          app = self.get_object()
   415          if not permissions.IsOwnerOrAdmin.has_object_permission(permissions.IsOwnerOrAdmin(),
   416                                                                  request, self, app):
   417              raise PermissionDenied()
   418  
   419          user = get_object_or_404(User, username=request.data['username'])
   420          assign_perm(self.perm, user, app)
   421          models.log_event(app, "User {} was granted access to {}".format(user, app))
   422          return Response(status=status.HTTP_201_CREATED)
   423  
   424      def destroy(self, request, **kwargs):
   425          app = get_object_or_404(models.App, id=self.kwargs['id'])
   426          user = get_object_or_404(User, username=kwargs['username'])
   427  
   428          perm_name = "api.{}".format(self.perm)
   429          if not user.has_perm(perm_name, app):
   430              raise PermissionDenied()
   431  
   432          if (user != request.user and
   433              not permissions.IsOwnerOrAdmin.has_object_permission(permissions.IsOwnerOrAdmin(),
   434                                                                   request, self, app)):
   435              raise PermissionDenied()
   436          remove_perm(self.perm, user, app)
   437          models.log_event(app, "User {} was revoked access to {}".format(user, app))
   438          return Response(status=status.HTTP_204_NO_CONTENT)
   439  
   440  
   441  class AdminPermsViewSet(BaseDeisViewSet):
   442      """RESTful views for sharing admin permissions with other users."""
   443  
   444      model = User
   445      serializer_class = serializers.AdminUserSerializer
   446      permission_classes = [permissions.IsAdmin]
   447  
   448      def get_queryset(self, **kwargs):
   449          self.check_object_permissions(self.request, self.request.user)
   450          return self.model.objects.filter(is_active=True, is_superuser=True)
   451  
   452      def create(self, request, **kwargs):
   453          user = get_object_or_404(User, username=request.data['username'])
   454          user.is_superuser = user.is_staff = True
   455          user.save(update_fields=['is_superuser', 'is_staff'])
   456          return Response(status=status.HTTP_201_CREATED)
   457  
   458      def destroy(self, request, **kwargs):
   459          user = get_object_or_404(User, username=kwargs['username'])
   460          user.is_superuser = user.is_staff = False
   461          user.save(update_fields=['is_superuser', 'is_staff'])
   462          return Response(status=status.HTTP_204_NO_CONTENT)
   463  
   464  
   465  class UserView(BaseDeisViewSet):
   466      """A Viewset for interacting with User objects."""
   467      model = User
   468      serializer_class = serializers.UserSerializer
   469      permission_classes = [permissions.IsAdmin]
   470  
   471      def get_queryset(self):
   472          return self.model.objects.exclude(username='AnonymousUser')