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