github.com/misfo/deis@v1.0.1-0.20141111224634-e0eee0392b8a/controller/api/views.py (about) 1 """ 2 RESTful view classes for presenting Deis API objects. 3 """ 4 5 from __future__ import absolute_import 6 from __future__ import unicode_literals 7 8 from django.conf import settings 9 from django.contrib.auth.models import AnonymousUser, User 10 from django.core.exceptions import ValidationError 11 from django.http import Http404 12 from django.utils import timezone 13 from guardian.shortcuts import assign_perm 14 from guardian.shortcuts import get_objects_for_user 15 from guardian.shortcuts import get_users_with_perms 16 from guardian.shortcuts import remove_perm 17 from rest_framework import permissions 18 from rest_framework import status 19 from rest_framework import viewsets 20 from rest_framework.authentication import BaseAuthentication 21 from rest_framework.exceptions import PermissionDenied 22 from rest_framework.generics import get_object_or_404 23 from rest_framework.response import Response 24 25 from api import models, serializers 26 from api.permissions import IsAnonymous, IsOwner, IsAppUser, \ 27 IsAdmin, HasRegistrationAuth, HasBuilderAuth 28 29 30 class AnonymousAuthentication(BaseAuthentication): 31 32 def authenticate(self, request): 33 """ 34 Authenticate the request and return a two-tuple of (user, token). 35 """ 36 user = AnonymousUser() 37 return user, None 38 39 40 class UserRegistrationView(viewsets.GenericViewSet, 41 viewsets.mixins.CreateModelMixin): 42 model = User 43 44 authentication_classes = (AnonymousAuthentication,) 45 permission_classes = (IsAnonymous, HasRegistrationAuth) 46 serializer_class = serializers.UserSerializer 47 48 def pre_save(self, obj): 49 """Replicate UserManager.create_user functionality.""" 50 now = timezone.now() 51 obj.last_login = now 52 obj.date_joined = now 53 obj.is_active = True 54 obj.email = User.objects.normalize_email(obj.email) 55 obj.set_password(obj.password) 56 # Make this first signup an admin / superuser 57 if not User.objects.filter(is_superuser=True).exists(): 58 obj.is_superuser = obj.is_staff = True 59 60 61 class UserManagementView(viewsets.GenericViewSet, 62 viewsets.mixins.CreateModelMixin, 63 viewsets.mixins.DestroyModelMixin): 64 model = User 65 permission_classes = (permissions.IsAuthenticated,) 66 67 def passwd(self, request, *args, **kwargs): 68 obj = self.request.user 69 if not obj.check_password(request.DATA['password']): 70 return Response("Current password did not match", status=status.HTTP_400_BAD_REQUEST) 71 obj.set_password(request.DATA['new_password']) 72 obj.save() 73 return Response({'status': 'password set'}) 74 75 def destroy(self, request, *args, **kwargs): 76 obj = self.request.user 77 obj.delete() 78 return Response(status=status.HTTP_204_NO_CONTENT) 79 80 81 class OwnerViewSet(viewsets.ModelViewSet): 82 """Scope views to an `owner` attribute.""" 83 84 permission_classes = (permissions.IsAuthenticated, IsOwner) 85 86 def pre_save(self, obj): 87 obj.owner = self.request.user 88 89 def get_queryset(self, **kwargs): 90 """Filter all querysets by an `owner` attribute. 91 """ 92 return self.model.objects.filter(owner=self.request.user) 93 94 95 class AppPermsViewSet(viewsets.ViewSet): 96 """RESTful views for sharing apps with collaborators.""" 97 98 model = models.App # models class 99 perm = 'use_app' # short name for permission 100 101 def list(self, request, **kwargs): 102 app = get_object_or_404(self.model, id=kwargs['id']) 103 perm_name = "api.{}".format(self.perm) 104 if request.user != app.owner and \ 105 not request.user.has_perm(perm_name, app) and \ 106 not request.user.is_superuser: 107 return Response(status=status.HTTP_403_FORBIDDEN) 108 usernames = [u.username for u in get_users_with_perms(app) 109 if u.has_perm(perm_name, app)] 110 return Response({'users': usernames}) 111 112 def create(self, request, **kwargs): 113 app = get_object_or_404(self.model, id=kwargs['id']) 114 if request.user != app.owner and not request.user.is_superuser: 115 return Response(status=status.HTTP_403_FORBIDDEN) 116 user = get_object_or_404(User, username=request.DATA['username']) 117 assign_perm(self.perm, user, app) 118 models.log_event(app, "User {} was granted access to {}".format(user, app)) 119 return Response(status=status.HTTP_201_CREATED) 120 121 def destroy(self, request, **kwargs): 122 app = get_object_or_404(self.model, id=kwargs['id']) 123 if request.user != app.owner and not request.user.is_superuser: 124 return Response(status=status.HTTP_403_FORBIDDEN) 125 user = get_object_or_404(User, username=kwargs['username']) 126 if user.has_perm(self.perm, app): 127 remove_perm(self.perm, user, app) 128 models.log_event(app, "User {} was revoked access to {}".format(user, app)) 129 return Response(status=status.HTTP_204_NO_CONTENT) 130 else: 131 return Response(status=status.HTTP_404_NOT_FOUND) 132 133 134 class AdminPermsViewSet(viewsets.ModelViewSet): 135 """RESTful views for sharing admin permissions with other users.""" 136 137 model = User 138 serializer_class = serializers.AdminUserSerializer 139 permission_classes = (IsAdmin,) 140 141 def get_queryset(self, **kwargs): 142 return self.model.objects.filter(is_active=True, is_superuser=True) 143 144 def create(self, request, **kwargs): 145 user = get_object_or_404(User, username=request.DATA['username']) 146 user.is_superuser = user.is_staff = True 147 user.save(update_fields=['is_superuser', 'is_staff']) 148 return Response(status=status.HTTP_201_CREATED) 149 150 def destroy(self, request, **kwargs): 151 user = get_object_or_404(User, username=kwargs['username']) 152 user.is_superuser = user.is_staff = False 153 user.save(update_fields=['is_superuser', 'is_staff']) 154 return Response(status=status.HTTP_204_NO_CONTENT) 155 156 157 class AppViewSet(OwnerViewSet): 158 """RESTful views for :class:`~api.models.App`.""" 159 160 model = models.App 161 serializer_class = serializers.AppSerializer 162 lookup_field = 'id' 163 permission_classes = (permissions.IsAuthenticated, IsAppUser) 164 165 def get_queryset(self, **kwargs): 166 """ 167 Filter Apps by `owner` attribute or the `api.use_app` permission. 168 """ 169 return super(AppViewSet, self).get_queryset(**kwargs) | \ 170 get_objects_for_user(self.request.user, 'api.use_app') 171 172 def post_save(self, app, created=False, **kwargs): 173 if created: 174 app.create() 175 176 def scale(self, request, **kwargs): 177 new_structure = {} 178 try: 179 for target, count in request.DATA.items(): 180 new_structure[target] = int(count) 181 except (TypeError, ValueError): 182 return Response('Invalid scaling format', 183 status=status.HTTP_400_BAD_REQUEST) 184 app = self.get_object() 185 try: 186 models.validate_app_structure(new_structure) 187 app.scale(request.user, new_structure) 188 except (EnvironmentError, ValidationError) as e: 189 return Response(str(e), status=status.HTTP_400_BAD_REQUEST) 190 except RuntimeError as e: 191 return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE) 192 return Response(status=status.HTTP_204_NO_CONTENT, 193 content_type='application/json') 194 195 def logs(self, request, **kwargs): 196 app = self.get_object() 197 try: 198 logs = app.logs() 199 except EnvironmentError: 200 return Response("No logs for {}".format(app.id), 201 status=status.HTTP_204_NO_CONTENT, 202 content_type='text/plain') 203 return Response(logs, status=status.HTTP_200_OK, 204 content_type='text/plain') 205 206 def run(self, request, **kwargs): 207 app = self.get_object() 208 command = request.DATA['command'] 209 try: 210 output_and_rc = app.run(self.request.user, command) 211 except EnvironmentError as e: 212 return Response(str(e), status=status.HTTP_400_BAD_REQUEST) 213 except RuntimeError as e: 214 return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE) 215 return Response(output_and_rc, status=status.HTTP_200_OK, 216 content_type='text/plain') 217 218 def destroy(self, request, **kwargs): 219 obj = get_object_or_404(self.model, id=kwargs['id']) 220 obj.delete() 221 return Response(status=status.HTTP_204_NO_CONTENT) 222 223 224 class BaseAppViewSet(viewsets.ModelViewSet): 225 226 permission_classes = (permissions.IsAuthenticated, IsAppUser) 227 228 def pre_save(self, obj): 229 obj.owner = self.request.user 230 231 def get_queryset(self, **kwargs): 232 app = get_object_or_404(models.App, id=self.kwargs['id']) 233 try: 234 self.check_object_permissions(self.request, app) 235 except PermissionDenied: 236 raise Http404("No {} matches the given query.".format( 237 self.model._meta.object_name)) 238 return self.model.objects.filter(app=app) 239 240 def get_object(self, *args, **kwargs): 241 obj = self.get_queryset().latest('created') 242 self.check_object_permissions(self.request, obj) 243 return obj 244 245 246 class AppBuildViewSet(BaseAppViewSet): 247 """RESTful views for :class:`~api.models.Build`.""" 248 249 model = models.Build 250 serializer_class = serializers.BuildSerializer 251 252 def post_save(self, build, created=False): 253 if created: 254 self.release = build.create(self.request.user) 255 256 def get_success_headers(self, data): 257 headers = super(AppBuildViewSet, self).get_success_headers(data) 258 headers.update({'X-Deis-Release': self.release.version}) 259 return headers 260 261 def create(self, request, *args, **kwargs): 262 app = get_object_or_404(models.App, id=self.kwargs['id']) 263 request._data = request.DATA.copy() 264 request.DATA['app'] = app 265 try: 266 return super(AppBuildViewSet, self).create(request, *args, **kwargs) 267 except RuntimeError as e: 268 return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE) 269 270 271 class AppConfigViewSet(BaseAppViewSet): 272 """RESTful views for :class:`~api.models.Config`.""" 273 274 model = models.Config 275 serializer_class = serializers.ConfigSerializer 276 277 def get_object(self, *args, **kwargs): 278 """Return the Config associated with the App's latest Release.""" 279 app = get_object_or_404(models.App, id=self.kwargs['id']) 280 try: 281 self.check_object_permissions(self.request, app) 282 return app.release_set.latest().config 283 except (PermissionDenied, models.Release.DoesNotExist): 284 raise Http404("No {} matches the given query.".format( 285 self.model._meta.object_name)) 286 287 def pre_save(self, config): 288 """merge the old config with the new""" 289 previous_config = config.app.config_set.latest() 290 config.owner = self.request.user 291 if previous_config: 292 config.owner = previous_config.owner 293 for attr in ['cpu', 'memory', 'tags', 'values']: 294 # Guard against migrations from older apps without fixes to 295 # JSONField encoding. 296 try: 297 data = getattr(previous_config, attr).copy() 298 except AttributeError: 299 data = {} 300 try: 301 new_data = getattr(config, attr).copy() 302 except AttributeError: 303 new_data = {} 304 data.update(new_data) 305 # remove config keys if we provided a null value 306 [data.pop(k) for k, v in new_data.items() if v is None] 307 setattr(config, attr, data) 308 309 def post_save(self, config, created=False): 310 if created: 311 release = config.app.release_set.latest() 312 self.release = release.new(self.request.user, config=config, build=release.build) 313 try: 314 config.app.deploy(self.request.user, self.release) 315 except RuntimeError: 316 self.release.delete() 317 raise 318 319 def get_success_headers(self, data): 320 headers = super(AppConfigViewSet, self).get_success_headers(data) 321 headers.update({'X-Deis-Release': self.release.version}) 322 return headers 323 324 def create(self, request, *args, **kwargs): 325 obj = self.get_object() 326 request.DATA['app'] = obj.app 327 try: 328 return super(AppConfigViewSet, self).create(request, *args, **kwargs) 329 except RuntimeError as e: 330 return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE) 331 332 333 class AppReleaseViewSet(BaseAppViewSet): 334 """RESTful views for :class:`~api.models.Release`.""" 335 336 model = models.Release 337 serializer_class = serializers.ReleaseSerializer 338 339 def get_object(self, *args, **kwargs): 340 """Get Release by version always.""" 341 return self.get_queryset(**kwargs).get(version=self.kwargs['version']) 342 343 def rollback(self, request, *args, **kwargs): 344 """ 345 Create a new release as a copy of the state of the compiled slug and 346 config vars of a previous release. 347 """ 348 try: 349 app = get_object_or_404(models.App, id=self.kwargs['id']) 350 release = app.release_set.latest() 351 version_to_rollback_to = release.version - 1 352 if request.DATA.get('version'): 353 version_to_rollback_to = int(request.DATA['version']) 354 new_release = release.rollback(request.user, version_to_rollback_to) 355 response = {'version': new_release.version} 356 return Response(response, status=status.HTTP_201_CREATED) 357 except EnvironmentError as e: 358 return Response(str(e), status=status.HTTP_400_BAD_REQUEST) 359 except RuntimeError as e: 360 new_release.delete() 361 return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE) 362 363 364 class AppContainerViewSet(BaseAppViewSet): 365 """RESTful views for :class:`~api.models.Container`.""" 366 367 model = models.Container 368 serializer_class = serializers.ContainerSerializer 369 370 def get_queryset(self, **kwargs): 371 qs = super(AppContainerViewSet, self).get_queryset(**kwargs) 372 container_type = self.kwargs.get('type') 373 if container_type: 374 qs = qs.filter(type=container_type) 375 else: 376 qs = qs.exclude(type='run') 377 return qs 378 379 def get_object(self, *args, **kwargs): 380 qs = self.get_queryset(**kwargs) 381 obj = qs.get(num=self.kwargs['num']) 382 return obj 383 384 385 class KeyViewSet(OwnerViewSet): 386 """RESTful views for :class:`~api.models.Key`.""" 387 388 model = models.Key 389 serializer_class = serializers.KeySerializer 390 lookup_field = 'id' 391 392 393 class DomainViewSet(OwnerViewSet): 394 """RESTful views for :class:`~api.models.Domain`.""" 395 396 model = models.Domain 397 serializer_class = serializers.DomainSerializer 398 399 def create(self, request, *args, **kwargs): 400 app = get_object_or_404(models.App, id=self.kwargs['id']) 401 request._data = request.DATA.copy() 402 request.DATA['app'] = app 403 return super(DomainViewSet, self).create(request, *args, **kwargs) 404 405 def get_queryset(self, **kwargs): 406 app = get_object_or_404(models.App, id=self.kwargs['id']) 407 qs = self.model.objects.filter(app=app) 408 return qs 409 410 def get_object(self, *args, **kwargs): 411 qs = self.get_queryset(**kwargs) 412 obj = qs.get(domain=self.kwargs['domain']) 413 return obj 414 415 416 class BaseHookViewSet(viewsets.ModelViewSet): 417 418 permission_classes = (HasBuilderAuth,) 419 420 def pre_save(self, obj): 421 # SECURITY: we trust the username field to map to the owner 422 obj.owner = self.request.DATA['owner'] 423 424 425 class PushHookViewSet(BaseHookViewSet): 426 """API hook to create new :class:`~api.models.Push`""" 427 428 model = models.Push 429 serializer_class = serializers.PushSerializer 430 431 def create(self, request, *args, **kwargs): 432 app = get_object_or_404(models.App, id=request.DATA['receive_repo']) 433 user = get_object_or_404( 434 User, username=request.DATA['receive_user']) 435 # check the user is authorized for this app 436 if user == app.owner or \ 437 user in get_users_with_perms(app) or \ 438 user.is_superuser: 439 request._data = request.DATA.copy() 440 request.DATA['app'] = app 441 request.DATA['owner'] = user 442 return super(PushHookViewSet, self).create(request, *args, **kwargs) 443 raise PermissionDenied() 444 445 446 class BuildHookViewSet(BaseHookViewSet): 447 """API hook to create new :class:`~api.models.Build`""" 448 449 model = models.Build 450 serializer_class = serializers.BuildSerializer 451 452 def create(self, request, *args, **kwargs): 453 app = get_object_or_404(models.App, id=request.DATA['receive_repo']) 454 self.user = get_object_or_404( 455 User, username=request.DATA['receive_user']) 456 # check the user is authorized for this app 457 if self.user == app.owner or \ 458 self.user in get_users_with_perms(app) or \ 459 self.user.is_superuser: 460 request._data = request.DATA.copy() 461 request.DATA['app'] = app 462 request.DATA['owner'] = self.user 463 try: 464 super(BuildHookViewSet, self).create(request, *args, **kwargs) 465 # return the application databag 466 response = {'release': {'version': app.release_set.latest().version}, 467 'domains': ['.'.join([app.id, settings.DEIS_DOMAIN])]} 468 return Response(response, status=status.HTTP_200_OK) 469 except RuntimeError as e: 470 return Response(str(e), status=status.HTTP_503_SERVICE_UNAVAILABLE) 471 raise PermissionDenied() 472 473 def post_save(self, build, created=False): 474 if created: 475 build.create(self.user) 476 477 478 class ConfigHookViewSet(BaseHookViewSet): 479 """API hook to grab latest :class:`~api.models.Config`""" 480 481 model = models.Config 482 serializer_class = serializers.ConfigSerializer 483 484 def create(self, request, *args, **kwargs): 485 app = get_object_or_404(models.App, id=request.DATA['receive_repo']) 486 user = get_object_or_404( 487 User, username=request.DATA['receive_user']) 488 # check the user is authorized for this app 489 if user == app.owner or \ 490 user in get_users_with_perms(app) or \ 491 user.is_superuser: 492 config = app.release_set.latest().config 493 serializer = self.get_serializer(config) 494 return Response(serializer.data, status=status.HTTP_200_OK) 495 raise PermissionDenied()