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