github.com/rochacon/deis@v1.0.2-0.20150903015341-6839b592a1ff/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 228 class BuildViewSet(ReleasableViewSet): 229 """A viewset for interacting with Build objects.""" 230 model = models.Build 231 serializer_class = serializers.BuildSerializer 232 233 def post_save(self, build): 234 self.release = build.create(self.request.user) 235 super(BuildViewSet, self).post_save(build) 236 237 238 class ConfigViewSet(ReleasableViewSet): 239 """A viewset for interacting with Config objects.""" 240 model = models.Config 241 serializer_class = serializers.ConfigSerializer 242 243 def post_save(self, config): 244 release = config.app.release_set.latest() 245 self.release = release.new(self.request.user, config=config, build=release.build) 246 try: 247 config.app.deploy(self.request.user, self.release) 248 except RuntimeError: 249 self.release.delete() 250 raise 251 252 253 class ContainerViewSet(AppResourceViewSet): 254 """A viewset for interacting with Container objects.""" 255 model = models.Container 256 serializer_class = serializers.ContainerSerializer 257 258 def get_queryset(self, **kwargs): 259 qs = super(ContainerViewSet, self).get_queryset(**kwargs) 260 container_type = self.kwargs.get('type') 261 if container_type: 262 qs = qs.filter(type=container_type) 263 else: 264 qs = qs.exclude(type='run') 265 return qs 266 267 def get_object(self, **kwargs): 268 qs = self.get_queryset(**kwargs) 269 return qs.get(num=self.kwargs['num']) 270 271 def restart(self, *args, **kwargs): 272 try: 273 containers = self.get_app().restart(**kwargs) 274 serializer = self.get_serializer(containers, many=True) 275 return Response(serializer.data, status=status.HTTP_200_OK) 276 except Exception as e: 277 return Response({'detail': str(e)}, status=status.HTTP_503_SERVICE_UNAVAILABLE) 278 279 280 class DomainViewSet(AppResourceViewSet): 281 """A viewset for interacting with Domain objects.""" 282 model = models.Domain 283 serializer_class = serializers.DomainSerializer 284 285 def get_object(self, **kwargs): 286 qs = self.get_queryset(**kwargs) 287 return qs.get(domain=self.kwargs['domain']) 288 289 290 class CertificateViewSet(BaseDeisViewSet): 291 """A viewset for interacting with Domain objects.""" 292 model = models.Certificate 293 serializer_class = serializers.CertificateSerializer 294 295 def get_object(self, **kwargs): 296 """Retrieve domain certificate by common name""" 297 qs = self.get_queryset(**kwargs) 298 return qs.get(common_name=self.kwargs['common_name']) 299 300 301 class KeyViewSet(BaseDeisViewSet): 302 """A viewset for interacting with Key objects.""" 303 model = models.Key 304 permission_classes = [IsAuthenticated, permissions.IsOwner] 305 serializer_class = serializers.KeySerializer 306 307 308 class ReleaseViewSet(AppResourceViewSet): 309 """A viewset for interacting with Release objects.""" 310 model = models.Release 311 serializer_class = serializers.ReleaseSerializer 312 313 def get_object(self, **kwargs): 314 """Get release by version always""" 315 return self.get_queryset(**kwargs).get(version=self.kwargs['version']) 316 317 def rollback(self, request, **kwargs): 318 """ 319 Create a new release as a copy of the state of the compiled slug and config vars of a 320 previous release. 321 """ 322 app = self.get_app() 323 try: 324 release = app.release_set.latest() 325 version_to_rollback_to = release.version - 1 326 if request.data.get('version'): 327 version_to_rollback_to = int(request.data['version']) 328 new_release = release.rollback(request.user, version_to_rollback_to) 329 response = {'version': new_release.version} 330 return Response(response, status=status.HTTP_201_CREATED) 331 except EnvironmentError as e: 332 return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST) 333 except RuntimeError: 334 new_release.delete() 335 raise 336 337 338 class BaseHookViewSet(BaseDeisViewSet): 339 permission_classes = [permissions.HasBuilderAuth] 340 341 342 class PushHookViewSet(BaseHookViewSet): 343 """API hook to create new :class:`~api.models.Push`""" 344 model = models.Push 345 serializer_class = serializers.PushSerializer 346 347 def create(self, request, *args, **kwargs): 348 app = get_object_or_404(models.App, id=request.data['receive_repo']) 349 request.user = get_object_or_404(User, username=request.data['receive_user']) 350 # check the user is authorized for this app 351 if not permissions.is_app_user(request, app): 352 raise PermissionDenied() 353 request.data['app'] = app 354 request.data['owner'] = request.user 355 return super(PushHookViewSet, self).create(request, *args, **kwargs) 356 357 358 class BuildHookViewSet(BaseHookViewSet): 359 """API hook to create new :class:`~api.models.Build`""" 360 model = models.Build 361 serializer_class = serializers.BuildSerializer 362 363 def create(self, request, *args, **kwargs): 364 app = get_object_or_404(models.App, id=request.data['receive_repo']) 365 self.user = request.user = get_object_or_404(User, username=request.data['receive_user']) 366 # check the user is authorized for this app 367 if not permissions.is_app_user(request, app): 368 raise PermissionDenied() 369 request.data['app'] = app 370 request.data['owner'] = self.user 371 super(BuildHookViewSet, self).create(request, *args, **kwargs) 372 # return the application databag 373 response = {'release': {'version': app.release_set.latest().version}, 374 'domains': ['.'.join([app.id, settings.DEIS_DOMAIN])]} 375 return Response(response, status=status.HTTP_200_OK) 376 377 def post_save(self, build): 378 build.create(self.user) 379 380 381 class ConfigHookViewSet(BaseHookViewSet): 382 """API hook to grab latest :class:`~api.models.Config`""" 383 model = models.Config 384 serializer_class = serializers.ConfigSerializer 385 386 def create(self, request, *args, **kwargs): 387 app = get_object_or_404(models.App, id=request.data['receive_repo']) 388 request.user = get_object_or_404(User, username=request.data['receive_user']) 389 # check the user is authorized for this app 390 if not permissions.is_app_user(request, app): 391 raise PermissionDenied() 392 config = app.release_set.latest().config 393 serializer = self.get_serializer(config) 394 return Response(serializer.data, status=status.HTTP_200_OK) 395 396 397 class AppPermsViewSet(BaseDeisViewSet): 398 """RESTful views for sharing apps with collaborators.""" 399 400 model = models.App # models class 401 perm = 'use_app' # short name for permission 402 403 def get_queryset(self): 404 return self.model.objects.all() 405 406 def list(self, request, **kwargs): 407 app = self.get_object() 408 perm_name = "api.{}".format(self.perm) 409 usernames = [u.username for u in get_users_with_perms(app) 410 if u.has_perm(perm_name, app)] 411 return Response({'users': usernames}) 412 413 def create(self, request, **kwargs): 414 app = self.get_object() 415 if not permissions.IsOwnerOrAdmin.has_object_permission(permissions.IsOwnerOrAdmin(), 416 request, self, app): 417 raise PermissionDenied() 418 419 user = get_object_or_404(User, username=request.data['username']) 420 assign_perm(self.perm, user, app) 421 models.log_event(app, "User {} was granted access to {}".format(user, app)) 422 return Response(status=status.HTTP_201_CREATED) 423 424 def destroy(self, request, **kwargs): 425 app = get_object_or_404(models.App, id=self.kwargs['id']) 426 user = get_object_or_404(User, username=kwargs['username']) 427 428 perm_name = "api.{}".format(self.perm) 429 if not user.has_perm(perm_name, app): 430 raise PermissionDenied() 431 432 if (user != request.user and 433 not permissions.IsOwnerOrAdmin.has_object_permission(permissions.IsOwnerOrAdmin(), 434 request, self, app)): 435 raise PermissionDenied() 436 remove_perm(self.perm, user, app) 437 models.log_event(app, "User {} was revoked access to {}".format(user, app)) 438 return Response(status=status.HTTP_204_NO_CONTENT) 439 440 441 class AdminPermsViewSet(BaseDeisViewSet): 442 """RESTful views for sharing admin permissions with other users.""" 443 444 model = User 445 serializer_class = serializers.AdminUserSerializer 446 permission_classes = [permissions.IsAdmin] 447 448 def get_queryset(self, **kwargs): 449 self.check_object_permissions(self.request, self.request.user) 450 return self.model.objects.filter(is_active=True, is_superuser=True) 451 452 def create(self, request, **kwargs): 453 user = get_object_or_404(User, username=request.data['username']) 454 user.is_superuser = user.is_staff = True 455 user.save(update_fields=['is_superuser', 'is_staff']) 456 return Response(status=status.HTTP_201_CREATED) 457 458 def destroy(self, request, **kwargs): 459 user = get_object_or_404(User, username=kwargs['username']) 460 user.is_superuser = user.is_staff = False 461 user.save(update_fields=['is_superuser', 'is_staff']) 462 return Response(status=status.HTTP_204_NO_CONTENT) 463 464 465 class UserView(BaseDeisViewSet): 466 """A Viewset for interacting with User objects.""" 467 model = User 468 serializer_class = serializers.UserSerializer 469 permission_classes = [permissions.IsAdmin] 470 471 def get_queryset(self): 472 return self.model.objects.exclude(username='AnonymousUser')