Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@
"rollup": "4.59.0",
"esbuild": "0.27.3",
"fast-uri": "^3.1.5",
"minimatch": ">=3.1.3",
"follow-redirects": ">=1.16.2",
"minimatch": ">=11.0.1",
"postcss": "^8.5.18",
"serialize-javascript": "7.0.3",
"workbox-build": {
Expand Down
6 changes: 3 additions & 3 deletions packages/factory-local/requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1235,9 +1235,9 @@ pygments==2.21.0 \
--hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
--hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
# via pytest
pypdf==6.21.2 \
--hash=sha256:0a3e1c5c886a1be03646c16a04827cc166c2a0575c971b26a4bf89686551c2d4 \
--hash=sha256:1d44d1a3a200f25062adff0085b02040e4646431ac2ccac22e42679e91466084
pypdf==6.21.4 \
--hash=sha256:33e4fb41ca352810eb4e9838aa0d82d6888a5f8bf682f43e99cce5faa666255c \
--hash=sha256:c8817e3f888d47a087872ded69812c1ef30cacb871d6e053e0c00bbaf0bc2163
# via -r requirements.txt
pytest==9.1.1 \
--hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \
Expand Down
2 changes: 1 addition & 1 deletion packages/factory-local/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ neo4j>=5.23,<6
streamlit>=1.38,<2
pytest>=9.0.3,<10
beautifulsoup4>=4.12,<5
pypdf>=6.21.2,<7
pypdf>=6.21.4,<7
openai>=1.40,<3
python-dotenv>=1.0,<2
2 changes: 1 addition & 1 deletion requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ beautifulsoup4>=4.15.0
lxml>=6.1.3
pdfminer.six>=20260107
pdfplumber>=0.11.10
pypdf>=6.21.2,<7 # CVE-2026-115572/115580/115585; CVE-2026-71852, CVE-2026-71870; earlier: CVE-2026-59935/59936 (inline-image DoS), CVE-2026-48155/48156, GHSA-jm82-fx9c-mx94
pypdf>=6.21.4,<7 # CVE-2026-119203/119204/119205 (6.21.4); CVE-2026-119206/119207/119208/119209 (6.21.3); earlier: CVE-2026-115572/115580/115585, CVE-2026-71852/71870, CVE-2026-59935/59936, CVE-2026-48155/48156, GHSA-jm82-fx9c-mx94
fpdf2>=2.8.9,<3
python-docx>=1.2.0
docx2txt>=0.9
Expand Down
12 changes: 6 additions & 6 deletions requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2999,9 +2999,9 @@ psycopg-binary==3.3.6 \
--hash=sha256:fa34eb47969297471db7b7f193622c7e3ee839ec05abd05f1fe104d5b1b1dcf4 \
--hash=sha256:fdccb3a0e184b03e9baa673b15a809cf36c339c85dbda0ebc25a698846dfbee8
# via psycopg
pyasn1==0.6.4 \
--hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \
--hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b
pyasn1==0.6.5 \
--hash=sha256:4017b0fce125096b8573b39c989fdb1a4b19cf761e683398184550fea0e66587 \
--hash=sha256:6b11616a6bcf3a40e635f082bb232fa57b5e0dd7f517bd7ddd69ec3794508a47
# via
# pyasn1-modules
# webauthn
Expand Down Expand Up @@ -3202,9 +3202,9 @@ pyotp==2.10.0 \
--hash=sha256:1df2f6a1bcc3bb0716172a5215ddc2f8c7c7fd26a13df9927d52e1746934836c \
--hash=sha256:d01e9703443616b03c57c700b5cbffd56a1f929c1b0f8f03131bc78c1fca9d3f
# via -r requirements.in
pypdf==6.21.2 \
--hash=sha256:0a3e1c5c886a1be03646c16a04827cc166c2a0575c971b26a4bf89686551c2d4 \
--hash=sha256:1d44d1a3a200f25062adff0085b02040e4646431ac2ccac22e42679e91466084
pypdf==6.21.4 \
--hash=sha256:33e4fb41ca352810eb4e9838aa0d82d6888a5f8bf682f43e99cce5faa666255c \
--hash=sha256:c8817e3f888d47a087872ded69812c1ef30cacb871d6e053e0c00bbaf0bc2163
# via -r requirements.in
pypdfium2==5.13.0 \
--hash=sha256:07f58e91b8c45ca144a1ff3008faf3c73ef8a5e9fb32988831788363288228cd \
Expand Down
77 changes: 40 additions & 37 deletions services/admin/app/user_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from sqlalchemy.orm import Session

from .audit import AuditLogger
from .database import get_session
from .database import arm_global_admin_context, disarm_global_admin_context, get_session
from .dependencies import PermissionChecker, get_current_user
from .models import TenantContext
from .sqlalchemy_models import MembershipModel, RoleModel, UserModel
Expand Down Expand Up @@ -120,45 +120,48 @@ async def list_users(
# drops every co-member and the directory returns only the caller. Permission
# Checker("users.read") already authorized this request; keep the query
# scoped by MembershipModel.tenant_id so the arm does not widen tenants.
# set_admin_context(true) is a no-op under SQLite.
TenantContext.set_admin_context(db, True)

# Count total
count_stmt = (
select(func.count())
.select_from(UserModel)
.join(MembershipModel, UserModel.id == MembershipModel.user_id)
.where(MembershipModel.tenant_id == tenant_id)
)
total = db.execute(count_stmt).scalar()

# helper for explicit join
stmt = (
select(UserModel, MembershipModel, RoleModel)
.join(MembershipModel, UserModel.id == MembershipModel.user_id)
.join(RoleModel, MembershipModel.role_id == RoleModel.id)
.where(MembershipModel.tenant_id == tenant_id)
.offset(pagination.skip)
.limit(pagination.limit)
)

rows = db.execute(stmt).all()
# Use transaction-local SET LOCAL (arm/disarm) so the GUC cannot leak past
# this handler if the pooled connection is reused before checkout reset.
arm_global_admin_context(db)
try:
Comment thread
PetrefiedThunder marked this conversation as resolved.
Resolved
# Count total
count_stmt = (
select(func.count())
.select_from(UserModel)
.join(MembershipModel, UserModel.id == MembershipModel.user_id)
.where(MembershipModel.tenant_id == tenant_id)
)
total = db.execute(count_stmt).scalar()

# helper for explicit join
stmt = (
select(UserModel, MembershipModel, RoleModel)
.join(MembershipModel, UserModel.id == MembershipModel.user_id)
.join(RoleModel, MembershipModel.role_id == RoleModel.id)
.where(MembershipModel.tenant_id == tenant_id)
.offset(pagination.skip)
.limit(pagination.limit)
)

results = []
for user, membership, role in rows:
results.append(
UserResponse(
id=user.id,
email=user.email,
status="inactive" if not membership.is_active else user.status,
is_sysadmin=user.is_sysadmin,
role_id=role.id,
role_name=role.name,
created_at=user.created_at,
rows = db.execute(stmt).all()

results = []
for user, membership, role in rows:
results.append(
UserResponse(
id=user.id,
email=user.email,
status="inactive" if not membership.is_active else user.status,
is_sysadmin=user.is_sysadmin,
role_id=role.id,
role_name=role.name,
created_at=user.created_at,
)
)
)

return PaginatedResponse(items=results, total=total, skip=pagination.skip, limit=pagination.limit)
return PaginatedResponse(items=results, total=total, skip=pagination.skip, limit=pagination.limit)
finally:
disarm_global_admin_context(db)


@router.patch(
Expand Down
9 changes: 5 additions & 4 deletions services/admin/tests/test_user_routes_1342.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,15 @@ def _execute_side_effect(*args, **kwargs):
call_count[0] += 1
result = MagicMock()
if call_count[0] == 1:
# set_admin_context arm (users FORCE RLS peer visibility)
# arm_global_admin_context (SET LOCAL is_sysadmin)
return result
if call_count[0] == 2:
# Count query
result.scalar.return_value = 1
else:
elif call_count[0] == 3:
# Rows query
result.all.return_value = [(user, membership, role)]
# call 4: disarm_global_admin_context in finally
Comment thread
PetrefiedThunder marked this conversation as resolved.
Resolved
return result

db.execute.side_effect = _execute_side_effect
Expand Down Expand Up @@ -147,7 +148,7 @@ def _execute_side_effect(*args, **kwargs):
return result
if call_count[0] == 2:
result.scalar.return_value = 1
else:
elif call_count[0] == 3:
result.all.return_value = [(user, membership, role)]
return result

Expand Down Expand Up @@ -177,7 +178,7 @@ def _execute_side_effect(*args, **kwargs):
return result
if call_count[0] == 2:
result.scalar.return_value = 0
else:
elif call_count[0] == 3:
result.all.return_value = []
return result

Expand Down
8 changes: 8 additions & 0 deletions tests/security/test_global_admin_context_allowlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@
"identity-global-self-service",
1,
),
# Tenant-scoped directory under PermissionChecker("users.read").
# Arm unlocks users_self_or_admin peer rows; MembershipModel.tenant_id
# still filters. SET LOCAL via arm/disarm (try/finally).
("services/admin/app/user_routes.py", "list_users"): (
"tenant-scoped-directory",
1,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
),
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
# Background coordination paths. These run only on ADMIN_DATABASE_URL and
# do not derive global authority from request metadata.
("services/shared/task_queue.py", "_arm_global_queue_transaction"): (
Expand Down Expand Up @@ -191,5 +198,6 @@ def test_allowlist_uses_only_reviewed_authority_classes():
"global-human",
"global-human-or-workload",
"identity-global-self-service",
"tenant-scoped-directory",
"worker-maintenance",
}
Loading
Loading