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