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