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