Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.
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
2 changes: 2 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const config: PlaywrightTestConfig = {
trace: "retain-on-failure",
video: "retain-on-failure",
screenshot: "only-on-failure",
/* Navigation timeout - increase for slower CI environments */
navigationTimeout: process.env.CI ? 60000 : 30000,
},
/* Maximum time one test can run for. */
timeout: process.env.CI ? 200 * 1000 : 60 * 1000,
Expand Down
51 changes: 26 additions & 25 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,27 +1,9 @@
[tool.black]
line-length = 100
target_version = ['py311']
target-version = ['py311']
include = '\.pyi?$'

[tool.ruff]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
# "I", # isort
"C", # flake8-comprehensions
"B", # flake8-bugbear
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"C901", # complexity
]

# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["A", "B", "C", "D", "E", "F"]
unfixable = []

# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
Expand All @@ -48,19 +30,38 @@ exclude = [
# Same as Black.
line-length = 100

# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

# Assume Python 3.11
target-version = "py311"

[tool.ruff.mccabe]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
# "I", # isort
"C", # flake8-comprehensions
"B", # flake8-bugbear
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"C901", # complexity
]

# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["A", "B", "C", "D", "E", "F"]
unfixable = []

# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[tool.ruff.lint.mccabe]
# Unlike Flake8, default to a complexity level of 10.
max-complexity = 10

[tool.ruff.isort]
[tool.ruff.lint.isort]
known-third-party = ["fastapi", "pydantic", "starlette"]

[tool.ruff.per-file-ignores]
[tool.ruff.lint.per-file-ignores]
"tests/conftest.py" = ["E402"]
"src/dispatch/entity/service.py" = ["W605"]
14 changes: 13 additions & 1 deletion src/dispatch/auth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,19 @@ def create_user(
return user


@user_router.get("/{user_id}", response_model=UserRead)
@user_router.get(
"/{user_id}",
dependencies=[
Depends(
PermissionsDependency(
[
OrganizationMemberPermission,
]
)
)
],
response_model=UserRead,
)
def get_user(db_session: DbSession, user_id: PrimaryKey):
"""Get a user."""
user = get(db_session=db_session, user_id=user_id)
Expand Down
66 changes: 37 additions & 29 deletions src/dispatch/database/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,6 @@ def apply_filters(query, filter_spec, model_cls=None, do_auto_join=True):

def apply_filter_specific_joins(model: Base, filter_spec: dict, query: orm.query):
"""Applies any model specific implicitly joins."""
print(f"Applying filter specific joins for model: {model} and filter_spec: {filter_spec}")
# this is required because by default sqlalchemy-filter's auto-join
# knows nothing about how to join many-many relationships.
model_map = {
Expand Down Expand Up @@ -689,7 +688,8 @@ def search_filter_sort_paginate(
sort = False if sort_by else True
query = search(query_str=query_str, query=query, model=model, sort=sort)

query_restricted = apply_model_specific_filters(model_cls, query, current_user, role)
# Apply model-specific filters directly to the query to avoid intersect ordering issues
query = apply_model_specific_filters(model_cls, query, current_user, role)

tag_all_filters = []
if filter_spec:
Expand All @@ -713,15 +713,9 @@ def search_filter_sort_paginate(
else:
query = apply_filters(query, filter_spec, model_cls)

if model == "Incident":
query = query.intersect(query_restricted)
for filter in tag_all_filters:
query = query.intersect(filter)

if model == "Case":
query = query.intersect(query_restricted)
for filter in tag_all_filters:
query = query.intersect(filter)
# Apply tag_all filters using intersect only when necessary
for filter in tag_all_filters:
query = query.intersect(filter)

if sort_by:
sort_spec = create_sort_spec(model, sort_by, descending)
Expand Down Expand Up @@ -808,35 +802,49 @@ def search_filter_sort_paginate(

def restricted_incident_filter(query: orm.Query, current_user: DispatchUser, role: UserRoles):
"""Adds additional incident filters to query (usually for permissions)."""
if role == UserRoles.member:
# We filter out restricted incidents for users with a member role if the user is not an incident participant
query = (
query.join(Participant, Incident.id == Participant.incident_id)
.join(IndividualContact)
.filter(
or_(
Incident.visibility == Visibility.open,
# Allow unrestricted access for admin roles
if role in [UserRoles.admin, UserRoles.owner, UserRoles.manager]:
return query.distinct()

# For all other roles (including member, none, and any unhandled roles),
# apply restrictive filtering - default deny approach
query = (
query.outerjoin(Participant, Incident.id == Participant.incident_id)
.outerjoin(IndividualContact, IndividualContact.id == Participant.individual_contact_id)
.filter(
or_(
Incident.visibility == Visibility.open,
and_(
Incident.visibility == Visibility.restricted,
IndividualContact.email == current_user.email,
)
),
)
)
)
return query.distinct()


def restricted_case_filter(query: orm.Query, current_user: DispatchUser, role: UserRoles):
"""Adds additional case filters to query (usually for permissions)."""
if role == UserRoles.member:
# We filter out restricted cases for users with a member role if the user is not a case participant
query = (
query.join(Participant, Case.id == Participant.case_id)
.join(IndividualContact)
.filter(
or_(
Case.visibility == Visibility.open,
# Allow unrestricted access for admin roles
if role in [UserRoles.admin, UserRoles.owner, UserRoles.manager]:
return query.distinct()

# For all other roles (including member, none, and any unhandled roles),
# apply restrictive filtering - default deny approach
query = (
query.outerjoin(Participant, Case.id == Participant.case_id)
.outerjoin(IndividualContact, IndividualContact.id == Participant.individual_contact_id)
.filter(
or_(
Case.visibility == Visibility.open,
and_(
Case.visibility == Visibility.restricted,
IndividualContact.email == current_user.email,
)
),
)
)
)
return query.distinct()


Expand Down
Loading
Loading