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')