def post(self, request: Request, project_id) -> Response: """ Update the group membership of multiple users at once. Users and groups as well as their memberships are global, therefore this action requires either superuser status or project tokens need to be in use. If the latter is the case, the requesting user is expected to have a) admin permission in the current project and is b) only allowed to change users and groups visible to them. """ action = request.POST.get('action') if action not in ('add', 'revoke'): raise ValueError('Action needs to be "add" or "revoke"') # Collect user and group information source_users = set( get_request_list(request.POST, 'source_users', [], int)) source_groups = set( get_request_list(request.POST, 'source_groups', [], int)) target_users = set( get_request_list(request.POST, 'target_users', [], int)) target_groups = set( get_request_list(request.POST, 'target_groups', [], int)) # Check permissions if settings.PROJECT_TOKEN_USER_VISIBILITY and not request.user.is_superuser: # Find all visible users and groups visible_user_ids = get_token_visible_users(request.user.id) visible_group_ids = get_token_visible_groups(request.user.id) invisible_user_ids = (set(source_users).union( set(target_users))).difference(set(visible_user_ids)) if invisible_user_ids: raise PermissionError( 'This request includes users beyond the allowed scope') elif not request.user.is_superuser: raise PermissionError('Need superuser permission') updated, warnings = update_group_memberships(action, source_users, source_groups, target_users, target_groups) return JsonResponse({ 'updated_users': updated, 'warnings': warnings, })
def user_list(request: HttpRequest) -> JsonResponse: """List registered users in this CATMAID instance. If accessed by an anonymous user, only the anonymous user is returned unless the anonymous user has can_browse permissions, which allows it to retrieve all users. An administrator can export users including their salted and encrpyted password. This is meant to import users into other CATMAID instances. --- parameters: - name: with_passwords description: | Export encrypted passwords. Requires admin access. required: false type: boolean, default: false """ with_passwords = get_request_bool(request.GET, 'with_passwords', False) if with_passwords: # Make sure user is an admin and part of the staff if not request.user.is_staff and not request.user.is_superuser: raise PermissionError("Superuser permissions required to export " "encrypted user passwords") user = request.user can_see_all_users = user.is_authenticated and \ (user != get_anonymous_user() or user.has_perm('catmaid.can_browse')) if can_see_all_users: result = [] for u in User.objects.all().select_related('userprofile') \ .order_by('last_name', 'first_name'): up = u.userprofile user_data = { "id": u.id, "login": u.username, "full_name": u.get_full_name(), "first_name": u.first_name, "last_name": u.last_name, "color": (up.color.r, up.color.g, up.color.b), "primary_group_id": up.primary_group_id, } if with_passwords: # Append encypted user password user_data['password'] = u.password result.append(user_data) else: up = user.userprofile result = [{ "id": user.id, "login": user.username, "full_name": user.get_full_name(), "first_name": user.first_name, "last_name": user.last_name, "color": (up.color.r, up.color.g, up.color.b), "primary_group_id": up.primary_group_id }] return JsonResponse(result, safe=False)
def head(self, request: Request, project_id, alias) -> Response: """Get a deep-links available to the client. --- serializer: DeepLinkSerializer """ try: deep_link = DeepLink.objects.get(project_id=project_id, alias=alias) if not deep_link.is_public and request.user != deep_link.user: raise PermissionError('Can not find or access link') return Response() except DeepLink.DoesNotExist: return Response('Link not found', status=status.HTTP_404_NOT_FOUND)
def label_remove(request, project_id=None): label_id = int(request.POST['label_id']) if request.user.is_superuser: try: label = ClassInstance.objects.get(id=label_id, class_column__class_name='label') except ClassInstance.DoesNotExist: raise ValueError("Could not find label with ID %s" % label_id) is_referenced = TreenodeClassInstance.objects.filter( class_instance_id=label_id).exists() if is_referenced: raise ValueError("Only unreferenced labels are allowed to be removed") else: label.delete() return JsonResponse({ 'deleted_labels': [label_id], 'message': 'success' }) raise PermissionError('Only super users can delete unreferenced labels')
def user_list(request: HttpRequest) -> JsonResponse: """List registered users in this CATMAID instance. Must be logged in. An administrator can export users including their encrpyted password. This is meant to import users into other CATMAID instances. --- parameters: - name: with_passwords description: | Export encrypted passwords. Requires admin access. required: false type: boolean, default: false """ with_passwords = get_request_bool(request.GET, 'with_passwords', False) if with_passwords: # Make sure user is an admin and part of the staff if not request.user.is_staff and not request.user.is_superuser: raise PermissionError("Superuser permissions required to export " "encrypted user passwords") result = [] for u in User.objects.all().select_related('userprofile') \ .order_by('last_name', 'first_name'): up = u.userprofile user_data = { "id": u.id, "login": u.username, "full_name": u.get_full_name(), "first_name": u.first_name, "last_name": u.last_name, "color": (up.color.r, up.color.g, up.color.b), "primary_group_id": up.primary_group_id, } if with_passwords: # Append encypted user password user_data['password'] = u.password result.append(user_data) return JsonResponse(result, safe=False)
def post(self, request: Request) -> Response: """Apply a project token. serializer: SimpleProjectTokenSerializer """ if request.user.is_anonymous: raise PermissionError("Anonymous users can't apply tokens") token = get_object_or_404(ProjectToken, token=request.POST.get('token')) favorite = get_request_bool(request.POST, 'favorite', True) if not token.enabled: raise ValueError("Can't apply token") for perm in token.default_permissions: assign_perm(perm, request.user, token.project) upt = UserProjectToken.objects.create( **{ 'user': request.user, 'project_token': token, 'enabled': not token.needs_approval, }) if favorite: fp = FavoriteProject.objects.create(**{ 'project_id': token.project_id, 'user_id': request.user.id, }) return Response({ 'project_id': token.project_id, 'project_name': token.project.title, 'permissions': token.default_permissions, 'needs_approval': token.needs_approval, })
def post(self, request: Request, project_id) -> Response: """Revoke a project token. --- parameters: - name: token required: true serializer: SimpleProjectTokenSerializer """ if request.user.is_anonymous: raise PermissionError("Anonymous users can't revoke tokens") token = get_object_or_404(ProjectToken, pk=request.POST.get('token_id')) for perm in token.default_permissions: remove_perm(perm, request.user, token.project) delete = UserProjectToken.objects.filter( project_token=token, user_id=request.user.id).delete() return Response({ 'delete': delete, })
def post(self, request: Request, project_id) -> Response: """Create a deep-link. The request user must not be anonymous and must have annotate permissions. --- serializer: DeepLinkSerializer """ if request.user == get_anonymous_user( ) or not request.user.is_authenticated: raise PermissionError('Unauthenticated or anonymous users ' \ 'can not create persistent deep links.') project_id = int(project_id) alias = request.POST.get('alias') if alias: if not re.match(r'^[a-zA-Z0-9-_\.]+$', alias): raise ValueError( "Only alphanumeric characters, '-', '_' and '.' allowed") else: n_links = DeepLink.objects.filter(project_id=project_id).count() alias = make_unique_id() params = { 'project_id': project_id, 'user': request.user, 'alias': alias, } if 'is_public' in request.POST: params['is_public'] = get_request_bool(request.POST, 'is_public') if 'location_x' in request.POST: params['location_x'] = float(request.POST['location_x']) if 'location_y' in request.POST: params['location_y'] = float(request.POST['location_y']) if 'location_z' in request.POST: params['location_z'] = float(request.POST['location_z']) if 'active_treenode_id' in request.POST: params['active_treenode_id'] = int( request.POST['active_treenode_id']) if 'active_connector_id' in request.POST: params['active_connector_id'] = int( request.POST['active_connector_id']) if 'active_skeleton_id' in request.POST: params['active_skeleton_id'] = int( request.POST['active_skeleton_id']) if 'closest_node_to_location' in request.POST: params['closest_node_to_location'] = get_request_bool( request.POST, 'closest_node_to_location') if 'follow_id_history' in request.POST: params['follow_id_history'] = get_request_bool( request.POST, 'follow_id_history') if 'layered_stacks' in request.POST: params['layered_stacks'] = get_request_bool( request.POST, 'layered_stacks') if 'layout' in request.POST: params['layout'] = request.POST['layout'] if 'tool' in request.POST: params['tool'] = request.POST['tool'] if 'show_help' in request.POST: params['show_help'] = get_request_bool(request.POST, 'show_help') if 'password' in request.POST: params['password'] = make_password(request.POST('password')) if 'message' in request.POST: params['message'] = request.POST['message'] # TBA: data_view deeplink = DeepLink(**params) deeplink.save() serializer = DeepLinkSerializer(deeplink) # Stacks stacks = get_request_list(request.POST, 'stacks', map_fn=float) if stacks: # Nested lists of 2-tuples: [[stack_id, scale_level]] for s in stacks: stack_link = DeepLinkStack( **{ 'project_id': project_id, 'user_id': request.user.id, 'deep_link': deeplink, 'stack_id': int(s[0]), 'zoom_level': s[1], }) stack_link.save() # Stack groups if 'stack_group' in request.POST: sg_id = int(request.POST['stack_group']) sg_zoom_levels = get_request_list(request.POST, 'stack_group_scale_levels', map_fn=float) sg_link = DeepLinkStackGroup( **{ 'project_id': project_id, 'user_id': request.user.id, 'deeplink': deeplink, 'stack_group_id': sg_id, 'zoom_levels': sg_zoom_levels, }) sg_link.save() return Response(serializer.data)
def user_list(request:HttpRequest) -> JsonResponse: """List registered users in this CATMAID instance. If accessed by an anonymous user, only the anonymous user is returned unless the anonymous user has can_browse permissions, which allows it to retrieve all users. If the settings.py setting PROJECT_TOKEN_USER_VISIBILITY = True, logged in users will only see those users that share project tokens with them. An administrator can export users including their salted and encrpyted password. This is meant to import users into other CATMAID instances. --- parameters: - name: with_passwords description: | Export encrypted passwords. Requires admin access. required: false type: boolean, default: false """ with_passwords = get_request_bool(request.GET, 'with_passwords', False) if with_passwords: # Make sure user is an admin and part of the staff if not request.user.is_staff and not request.user.is_superuser: raise PermissionError("Superuser permissions required to export " "encrypted user passwords") user = request.user anon_user = get_anonymous_user() user_list = [] # Super users get to see all users, regardless of the backend setting. if settings.PROJECT_TOKEN_USER_VISIBILITY and not user.is_superuser: cursor = connection.cursor() cursor.execute(""" WITH project_tokens AS ( SELECT DISTINCT project_token_id AS id FROM catmaid_user_project_token WHERE user_id = %(user_id)s UNION SELECT id FROM catmaid_project_token WHERE user_id = %(user_id)s ) SELECT DISTINCT ON (au.id) au.id, au.username, au.first_name, au.last_name, (up.color).r, (up.color).g, (up.color).b, up.primary_group_id FROM project_tokens pt JOIN catmaid_user_project_token upt ON pt.id = upt.project_token_id JOIN auth_user au ON au.id = upt.user_id JOIN catmaid_userprofile up ON up.user_id = au.id UNION SELECT au.id, au.username, au.first_name, au.last_name, (up.color).r, (up.color).g, (up.color).b, up.primary_group_id FROM auth_user au JOIN catmaid_userprofile up ON up.user_id = au.id WHERE au.id = %(user_id)s OR au.id = %(anon_user_id)s """, { 'user_id': user.id, 'anon_user_id': anon_user.id, }) user_list = list(map(lambda u: { "id": u[0], "login": u[1], "full_name": f'{u[2]} {u[3]}', "first_name": u[2], "last_name": u[3], "color": (u[4], u[5], u[6]), "primary_group_id": u[7], }, cursor.fetchall())) else: can_see_all_users = user.is_authenticated and \ (user != anon_user or user.has_perm('catmaid.can_browse')) if can_see_all_users: for u in User.objects.all().select_related('userprofile') \ .order_by('last_name', 'first_name'): up = u.userprofile user_data = { "id": u.id, "login": u.username, "full_name": u.get_full_name(), "first_name": u.first_name, "last_name": u.last_name, "color": (up.color.r, up.color.g, up.color.b), "primary_group_id": up.primary_group_id, } if with_passwords: # Append encypted user password user_data['password'] = u.password user_list.append(user_data) if not user_list: up = user.userprofile user_list = [{ "id": user.id, "login": user.username, "full_name": user.get_full_name(), "first_name": user.first_name, "last_name": user.last_name, "color": (up.color.r, up.color.g, up.color.b), "primary_group_id": up.primary_group_id }] return JsonResponse(user_list, safe=False)
def get(self, request: HttpRequest, project_id, pointcloud_id) -> JsonResponse: """Return a point cloud. parameters: - name: project_id description: Project of the returned point cloud type: integer paramType: path required: true - name: simple description: Wheter or not only ID and name should be returned type: bool paramType: form required: false defaultValue: false - name: with_images description: Wheter linked images should returned as well. type: bool paramType: form required: false defaultValue: false - name: with_points description: Wheter linked points should returned as well. type: bool paramType: form required: false defaultValue: false """ with_images = get_request_bool(request.query_params, 'with_images', False) with_points = get_request_bool(request.query_params, 'with_points', False) sample_ratio = float(request.query_params.get('sample_ratio', '1.0')) simple = get_request_bool(request.query_params, 'simple', False) pointcloud = PointCloud.objects.get(pk=pointcloud_id, project_id=project_id) pointcloud_data = serialize_pointcloud(pointcloud, simple) # Check permissions. If there are no read permission assigned at all, # everyone can read. if 'can_read' not in get_perms(request.user, pointcloud) and \ len(get_users_with_perms(pointcloud)) > 0: raise PermissionError( f'User "{request.user.username}" not allowed to read point cloud #{pointcloud.id}' ) if with_images: images = [serialize_image_data(i) for i in pointcloud.images.all()] pointcloud_data['images'] = images if with_points: if sample_ratio == 1.0: points = [ serialize_point(p, compact=True) for p in pointcloud.points.all() ] pointcloud_data['points'] = points else: n_points = PointCloudPoint.objects.filter( pointcloud_id=pointcloud.id).count() n_sample = int(n_points * sample_ratio) cursor = connection.cursor() # Select a random sample of N points in a repeatable fashion. cursor.execute( """ SELECT setseed(0); SELECT id, location_x, location_y, location_z FROM point p JOIN ( SELECT pcp.point_id FROM pointcloud_point pcp WHERE pcp.pointcloud_id = %(pointcloud_id)s ORDER BY random() ) ordered_points(id) USING(id) LIMIT %(n_sample)s """, { 'pointcloud_id': pointcloud.id, 'n_sample': n_sample }) pointcloud_data['points'] = cursor.fetchall() return JsonResponse(pointcloud_data)
def get(self, request: HttpRequest, project_id, pointcloud_id, image_id) -> HttpResponse: """Return a point cloud. parameters: - name: project_id description: Project of the returned point cloud image type: integer paramType: path required: true - name: pointcloud_id description: Point cloud this image is linked to type: integer paramType: path required: true - name: image_id description: The image to load type: integer paramType: path required: true """ pointcloud = PointCloud.objects.get(pk=pointcloud_id, project_id=project_id) # Check permissions. If there are no read permission assigned at all, # everyone can read. if 'can_read' not in get_perms(request.user, pointcloud) and \ len(get_users_with_perms(pointcloud)) > 0: raise PermissionError( f'User "{request.user.username}" not allowed to read point cloud #{pointcloud.id}' ) cursor = connection.cursor() cursor.execute( """ SELECT image, content_type, name FROM image_data img JOIN pointcloud_image_data pcid ON img.id = pcid.image_data_id WHERE img.project_id = %(project_id)s AND pcid.image_data_id = %(image_id)s AND pcid.pointcloud_id = %(pointcloud_id)s """, { 'project_id': project_id, 'pointcloud_id': pointcloud_id, 'image_id': image_id, }) rows = cursor.fetchall() if len(rows) == 0: raise ValueError("Could not find image") if len(rows) > 1: raise ValueError("Found more than one image") image_data = rows[0][0] content_type = rows[0][1] name = rows[0][2] response = HttpResponse(image_data.tobytes(), content_type=content_type) response['Content-Disposition'] = f'attachment; filename={name}' return response