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