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