Skip to content
Merged
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.281"
VERSION = "0.261.284"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
248 changes: 248 additions & 0 deletions application/single_app/functions_control_center_groups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
# functions_control_center_groups.py
"""Server-side group inventory and validated selection for Control Center."""

Check warning on line 2 in application/single_app/functions_control_center_groups.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

import re
from datetime import datetime, timezone


GROUP_STATUSES = ("active", "locked", "upload_disabled", "inactive")

Check warning on line 8 in application/single_app/functions_control_center_groups.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
GROUP_SORTS = ("name", "owner", "members", "documents", "tokens", "last_activity")

Check warning on line 9 in application/single_app/functions_control_center_groups.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
GROUP_BULK_LIMIT = 500
GROUP_SNAPSHOT_TTL = 90
GROUP_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")

Check warning on line 12 in application/single_app/functions_control_center_groups.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
GROUP_FILTER_KEYS = {
"search", "status", "status_filter", "owner", "members_min", "members_max",

Check warning on line 14 in application/single_app/functions_control_center_groups.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
"has_documents", "created_from", "created_to", "activity_from", "activity_to",
}


class GroupRequestError(ValueError):
"""A reviewed, user-safe validation message, distinct from storage failures."""

Check warning on line 20 in application/single_app/functions_control_center_groups.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.


def validate_group_id(value):

Check warning on line 23 in application/single_app/functions_control_center_groups.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
if not isinstance(value, str) or not GROUP_ID_PATTERN.fullmatch(value):
raise GroupRequestError("Invalid group or member ID.")
return value


def parse_group_filters(values):
"""Accept only documented filters; never interpolate caller values into SQL."""
filters = {}
for key in ("search", "owner"):

Check warning on line 32 in application/single_app/functions_control_center_groups.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
value = values.get(key, "")
if not isinstance(value, str) or len(value) > 200:
raise GroupRequestError("Search and owner filters may not exceed 200 characters.")

Check warning on line 35 in application/single_app/functions_control_center_groups.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
filters[key] = value.strip().casefold()
filters["status"] = values.get("status", values.get("status_filter", "all"))
if filters["status"] not in ("all", *GROUP_STATUSES):
raise GroupRequestError("Invalid group status filter.")
filters["has_documents"] = values.get("has_documents", "all")
if filters["has_documents"] not in ("all", "yes", "no"):
raise GroupRequestError("Invalid documents filter.")
for key in ("members_min", "members_max"):
raw = values.get(key)
if raw in (None, ""):
filters[key] = None
elif isinstance(raw, bool) or not re.fullmatch(r"\d{1,7}", str(raw)):
raise GroupRequestError("Member bounds must be non-negative whole numbers.")
else:
filters[key] = int(raw)
if (filters["members_min"] is not None and filters["members_max"] is not None
and filters["members_min"] > filters["members_max"]):
raise GroupRequestError("Minimum members may not exceed maximum members.")
for prefix in ("created", "activity"):
for suffix in ("from", "to"):

Check warning on line 55 in application/single_app/functions_control_center_groups.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains dynamic execution, persistence, or system access marker. Recommendation%3A Do not execute changed lifecycle scripts or installers while this finding is unresolved.
key = f"{prefix}_{suffix}"
raw = values.get(key)
if not raw:
filters[key] = None
continue
if not isinstance(raw, str) or not re.fullmatch(r"\d{4}-\d{2}-\d{2}", raw):
raise GroupRequestError("Date filters must use YYYY-MM-DD.")
try:
datetime.strptime(raw, "%Y-%m-%d")
except ValueError as ex:
raise GroupRequestError("Invalid calendar date.") from ex
filters[key] = raw
if filters[f"{prefix}_from"] and filters[f"{prefix}_to"]:
if filters[f"{prefix}_from"] > filters[f"{prefix}_to"]:
raise GroupRequestError("Date range start may not follow its end.")
filters["sort"] = values.get("sort", "name")
filters["direction"] = values.get("direction", "asc")
if filters["sort"] not in GROUP_SORTS or filters["direction"] not in ("asc", "desc"):
raise GroupRequestError("Invalid group sort or direction.")
return filters


def group_members(group):
"""Project actual membership roles, including an owner omitted from legacy users."""
owner = group.get("owner") or {}
users = list(group.get("users") or [])
if owner.get("id") and not any(user.get("userId") == owner["id"] for user in users):
users.append({"userId": owner["id"], **{k: owner.get(k, "") for k in ("email", "displayName")}})
members = {}
for user in users:
user_id = user.get("userId")
if not user_id:
continue
role = ("Owner" if user_id == owner.get("id") else
"Admin" if user_id in (group.get("admins") or []) else
"DocumentManager" if user_id in (group.get("documentManagers") or []) else "User")
members[user_id] = {
"id": user_id, "display_name": user.get("displayName", ""),
"email": user.get("email", ""), "role": role,
}
return list(members.values())


def group_row(group, documents, tokens, last_activity):
owner = group.get("owner") or {}
metrics = group.get("metrics") or {}
return {
"id": group["id"],
"name": group.get("name") or "",
"description": group.get("description") or "",
"owner": {
"id": owner.get("id"), "email": owner.get("email") or "",
"display_name": owner.get("displayName") or owner.get("display_name") or "",
},
"status": group.get("status") if group.get("status") in GROUP_STATUSES else "active",
"members": len(group_members(group)),
"documents": documents,
"tokens": tokens,
"created_at": group.get("createdDate"),
"last_activity": last_activity,
"metrics_calculated_at": metrics.get("calculated_at"),
}


def load_group_inventory(groups_container, documents_container, activity_container):
"""Four batched queries, independent of row count. Fail rather than invent totals."""
groups = list(groups_container.query_items(
query=("SELECT c.id, c.name, c.description, c.owner, c.users, c.admins, "
"c.documentManagers, c.status, c.createdDate, c.metrics FROM c"),
enable_cross_partition_query=True,
))
documents = {
row["group_id"]: int(row.get("total") or 0)
for row in documents_container.query_items(
query=("SELECT c.group_id, COUNT(1) AS total FROM c "
"WHERE c.type = 'document_metadata' AND IS_DEFINED(c.group_id) GROUP BY c.group_id"),
enable_cross_partition_query=True,
)
}
tokens = {
row["group_id"]: int(row.get("total") or 0)
for row in activity_container.query_items(
query=("SELECT c.workspace_context.group_id AS group_id, SUM(c.usage.total_tokens) AS total "
"FROM c WHERE c.activity_type = 'token_usage' "
"AND IS_DEFINED(c.workspace_context.group_id) AND IS_NUMBER(c.usage.total_tokens) "
"GROUP BY c.workspace_context.group_id"),
enable_cross_partition_query=True,
)
}
# The writers use three group locations. One coalesced expression avoids duplicate
# records and includes admin CSV and approval events that use top-level group_id.
group_expression = (
"IIF(IS_STRING(c.group_id) AND c.group_id != '', c.group_id, "
"IIF(IS_STRING(c.group.group_id) AND c.group.group_id != '', "
"c.group.group_id, c.workspace_context.group_id))"
)
activity_times = {
row["group_id"]: row.get("last_activity")
for row in activity_container.query_items(
query=(f"SELECT {group_expression} AS group_id, MAX(c.timestamp) AS last_activity FROM c "
f"WHERE IS_STRING({group_expression}) AND {group_expression} != '' "
f"GROUP BY {group_expression}"),
enable_cross_partition_query=True,
)
}
calculated_at = datetime.now(timezone.utc).isoformat()
return {
"rows": [group_row(group, documents.get(group["id"], 0), tokens.get(group["id"], 0),
activity_times.get(group["id"])) for group in groups],
"calculated_at": calculated_at,
}


def filter_group_inventory(rows, filters):
"""Filter before sorting or paging; missing dates do not match a date window."""
def matches(row):
if filters["search"] and filters["search"] not in f"{row['name']} {row['description']}".casefold():
return False
if filters["status"] != "all" and row["status"] != filters["status"]:
return False
owner = row["owner"]
if filters["owner"] and filters["owner"] not in (
f"{owner['id'] or ''} {owner['email']} {owner['display_name']}".casefold()
):
return False
for key, comparator in (("members_min", lambda x, y: x < y), ("members_max", lambda x, y: x > y)):
if filters[key] is not None and comparator(row["members"], filters[key]):
return False
if filters["has_documents"] == "yes" and row["documents"] == 0:
return False
if filters["has_documents"] == "no" and row["documents"] != 0:
return False
for prefix, field in (("created", "created_at"), ("activity", "last_activity")):
date = str(row.get(field) or "")[:10]
if filters[f"{prefix}_from"] and (not date or date < filters[f"{prefix}_from"]):
return False
if filters[f"{prefix}_to"] and (not date or date > filters[f"{prefix}_to"]):
return False
return True

selected = [row for row in rows if matches(row)]
field = filters["sort"]
def sort_value(row):
value = row["owner"]["display_name"] or row["owner"]["email"] if field == "owner" else row.get(field)
return value.casefold() if isinstance(value, str) else value
present = [row for row in selected if sort_value(row) is not None]
missing = [row for row in selected if sort_value(row) is None]
present.sort(key=lambda row: row["id"])
present.sort(key=sort_value, reverse=filters["direction"] == "desc")
missing.sort(key=lambda row: row["id"])
return present + missing


def validate_group_status_payload(data):
if not isinstance(data, dict):
raise GroupRequestError("A JSON object is required.")
status = data.get("status")
reason = data.get("reason", "")
if status not in GROUP_STATUSES:
raise GroupRequestError("Invalid group status.")
if not isinstance(reason, str) or len(reason) > 2000:
raise GroupRequestError("Reason must be text no longer than 2000 characters.")
reason = reason.strip()
if status in ("locked", "inactive") and not reason:
raise GroupRequestError("A reason is required for locked or inactive status.")
return status, reason


def select_group_bulk_ids(data, inventory_loader):
"""Resolve exclusions and cap the complete population before any mutation."""
if ("group_ids" in data) == ("filter" in data):
raise GroupRequestError("Provide either group_ids or a filter selection.")
if "group_ids" in data:
ids = data["group_ids"]
if not isinstance(ids, list) or len(ids) > GROUP_BULK_LIMIT:
raise GroupRequestError("Bulk actions are limited to 500 groups.")
ids = list(dict.fromkeys(validate_group_id(value) for value in ids))
else:
values = data["filter"]
exclusions = data.get("exclude_ids", [])
if not isinstance(values, dict) or set(values) - GROUP_FILTER_KEYS:
raise GroupRequestError("Invalid group selection filters.")
if not isinstance(exclusions, list) or len(exclusions) > GROUP_BULK_LIMIT:
raise GroupRequestError("Invalid exclusion list.")
excluded = {validate_group_id(value) for value in exclusions}
filters = parse_group_filters(values)
ids = [row["id"] for row in filter_group_inventory(inventory_loader()["rows"], filters)
if row["id"] not in excluded]
if not ids:
raise GroupRequestError("Select at least one group.")
if len(ids) > GROUP_BULK_LIMIT:
raise GroupRequestError("Bulk actions are limited to 500 groups.")
return ids
Loading
Loading