snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
This commit is contained in:
201
apps/api/plane/utils/order_queryset.py
Normal file
201
apps/api/plane/utils/order_queryset.py
Normal file
@@ -0,0 +1,201 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
from django.db.models import Case, CharField, Min, Value, When
|
||||
|
||||
# Custom ordering for priority and state
|
||||
PRIORITY_ORDER = ["urgent", "high", "medium", "low", "none"]
|
||||
STATE_ORDER = ["backlog", "unstarted", "started", "completed", "cancelled"]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# order_by allowlists — one per model/endpoint family
|
||||
# All contain bare field names (no leading '-'); the sanitizer strips the
|
||||
# prefix before looking up, so descending variants are implicitly covered.
|
||||
# Prevents ORM order_by injection via user-supplied query params.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ISSUE_ORDER_BY_ALLOWLIST = frozenset({
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"sequence_id",
|
||||
"sort_order",
|
||||
"target_date",
|
||||
"start_date",
|
||||
"completed_at",
|
||||
"archived_at",
|
||||
"priority",
|
||||
"state__name",
|
||||
"state__group",
|
||||
"assignees__first_name",
|
||||
"labels__name",
|
||||
"issue_module__module__name",
|
||||
})
|
||||
|
||||
# IntakeIssue queryset — fields are prefixed with `issue__` for the join.
|
||||
INTAKE_ISSUE_ORDER_BY_ALLOWLIST = frozenset({
|
||||
"issue__created_at",
|
||||
"issue__updated_at",
|
||||
"issue__sequence_id",
|
||||
"issue__sort_order",
|
||||
"issue__target_date",
|
||||
"issue__start_date",
|
||||
"issue__priority",
|
||||
"issue__state__name",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"status",
|
||||
})
|
||||
|
||||
# IssueActivity queryset (user activity, workspace member activity).
|
||||
ACTIVITY_ORDER_BY_ALLOWLIST = frozenset({
|
||||
"created_at",
|
||||
"updated_at",
|
||||
})
|
||||
|
||||
# Project list queryset.
|
||||
PROJECT_ORDER_BY_ALLOWLIST = frozenset({
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"name",
|
||||
"network",
|
||||
"sort_order",
|
||||
})
|
||||
|
||||
# Saved view (IssueView) list queryset.
|
||||
VIEW_ORDER_BY_ALLOWLIST = frozenset({
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"name",
|
||||
})
|
||||
|
||||
# Notification queryset.
|
||||
NOTIFICATION_ORDER_BY_ALLOWLIST = frozenset({
|
||||
"created_at",
|
||||
"updated_at",
|
||||
})
|
||||
|
||||
# Page list queryset.
|
||||
PAGE_ORDER_BY_ALLOWLIST = frozenset({
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"name",
|
||||
"sort_order",
|
||||
})
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# group_by / sub_group_by allowlist for Issue querysets — used by
|
||||
# GroupedOffsetPaginator / SubGroupedOffsetPaginator (plane/utils/paginator.py),
|
||||
# which pass the field name straight into F(), .values(), .order_by(), and
|
||||
# Window partition_by. Prevents unauthenticated ORM field-name injection via
|
||||
# user-supplied query params.
|
||||
# ---------------------------------------------------------------------------
|
||||
ISSUE_GROUP_BY_ALLOWLIST = frozenset({
|
||||
"state_id",
|
||||
"state__group",
|
||||
"priority",
|
||||
"labels__id",
|
||||
"assignees__id",
|
||||
"issue_module__module_id",
|
||||
"cycle_id",
|
||||
"project_id",
|
||||
"created_by",
|
||||
"target_date",
|
||||
"start_date",
|
||||
})
|
||||
|
||||
# Cycle list queryset.
|
||||
CYCLE_ORDER_BY_ALLOWLIST = frozenset({
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"name",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"sort_order",
|
||||
})
|
||||
|
||||
# Module list queryset.
|
||||
MODULE_ORDER_BY_ALLOWLIST = frozenset({
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"name",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"status",
|
||||
"sort_order",
|
||||
})
|
||||
|
||||
|
||||
def sanitize_order_by(value, allowed_fields, default="-created_at"):
|
||||
"""Return a safe ordering string derived from *value*.
|
||||
|
||||
Strips at most one leading '-' (descending indicator), checks the bare
|
||||
field name against *allowed_fields*, and reconstructs the value. Inputs
|
||||
with multiple leading dashes (e.g. ``--created_at``) are rejected and
|
||||
*default* is returned instead, preventing both allowlist bypass and
|
||||
malformed tokens from reaching ``.order_by()``.
|
||||
|
||||
Call this before passing any user-supplied ordering string to .order_by() or
|
||||
a paginator to prevent ORM order_by injection.
|
||||
"""
|
||||
if not value:
|
||||
return default
|
||||
is_desc = value.startswith("-")
|
||||
bare = value[1:] if is_desc else value
|
||||
# Reject malformed prefixes like "--created_at".
|
||||
if bare.startswith("-"):
|
||||
return default
|
||||
if bare not in allowed_fields:
|
||||
return default
|
||||
return f"-{bare}" if is_desc else bare
|
||||
|
||||
|
||||
def order_issue_queryset(issue_queryset, order_by_param="-created_at"):
|
||||
# Reject any field that is not in the allowlist before building the queryset.
|
||||
# An unrecognised value is silently replaced with the safe default so callers
|
||||
# receive consistent output rather than an ORM error or data leak.
|
||||
order_by_param = sanitize_order_by(order_by_param, ISSUE_ORDER_BY_ALLOWLIST, default="-created_at")
|
||||
# Priority Ordering
|
||||
if order_by_param == "priority" or order_by_param == "-priority":
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
priority_order=Case(
|
||||
*[When(priority=p, then=Value(i)) for i, p in enumerate(PRIORITY_ORDER)],
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("priority_order", "-created_at")
|
||||
order_by_param = "priority_order" if order_by_param.startswith("-") else "-priority_order"
|
||||
# State Ordering
|
||||
elif order_by_param in ["state__group", "-state__group"]:
|
||||
state_order = STATE_ORDER if order_by_param in ["state__name", "state__group"] else STATE_ORDER[::-1]
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
state_order=Case(
|
||||
*[When(state__group=state_group, then=Value(i)) for i, state_group in enumerate(state_order)],
|
||||
default=Value(len(state_order)),
|
||||
output_field=CharField(),
|
||||
)
|
||||
).order_by("state_order", "-created_at")
|
||||
order_by_param = "-state_order" if order_by_param.startswith("-") else "state_order"
|
||||
# assignee and label ordering
|
||||
elif order_by_param in [
|
||||
"labels__name",
|
||||
"assignees__first_name",
|
||||
"issue_module__module__name",
|
||||
"-labels__name",
|
||||
"-assignees__first_name",
|
||||
"-issue_module__module__name",
|
||||
]:
|
||||
issue_queryset = issue_queryset.annotate(
|
||||
min_values=Min(order_by_param[1::] if order_by_param.startswith("-") else order_by_param)
|
||||
).order_by(
|
||||
"-min_values" if order_by_param.startswith("-") else "min_values",
|
||||
"-created_at",
|
||||
)
|
||||
order_by_param = "-min_values" if order_by_param.startswith("-") else "min_values"
|
||||
else:
|
||||
# If the order_by_param is created_at, then don't add the -created_at
|
||||
if "created_at" in order_by_param:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||
else:
|
||||
issue_queryset = issue_queryset.order_by(order_by_param, "-created_at")
|
||||
order_by_param = order_by_param
|
||||
return issue_queryset, order_by_param
|
||||
Reference in New Issue
Block a user