Exemple #1
0
    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,
        })
Exemple #2
0
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)
Exemple #3
0
 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)
Exemple #4
0
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')
Exemple #5
0
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)
Exemple #6
0
    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,
        })
Exemple #7
0
    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,
        })
Exemple #8
0
    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)
Exemple #9
0
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)
Exemple #10
0
    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)
Exemple #11
0
    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