From 051e7c55e966fe5672058c6c4450cd791cf74286 Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:03:05 -0800 Subject: [PATCH 01/15] Check if challenge is already approved before raising exception (#5445) --- src/dispatch/plugins/dispatch_core/plugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dispatch/plugins/dispatch_core/plugin.py b/src/dispatch/plugins/dispatch_core/plugin.py index 1bb18284799e..ec3db480c076 100644 --- a/src/dispatch/plugins/dispatch_core/plugin.py +++ b/src/dispatch/plugins/dispatch_core/plugin.py @@ -379,6 +379,9 @@ def validate_mfa_token( raise ActionMismatchError("Action mismatch") if not challenge.valid: raise ExpiredChallengeError("Challenge is no longer valid") + if challenge.status == MfaChallengeStatus.APPROVED: + # Challenge has already been approved + return challenge.status if challenge.status != MfaChallengeStatus.PENDING: raise InvalidChallengeStateError(f"Challenge is in invalid state: {challenge.status}") From 2f1794826de8aaa1a37e4eb07b5a724c81f8ccaf Mon Sep 17 00:00:00 2001 From: Will Sheldon <114631109+wssheldon@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:30:36 -0800 Subject: [PATCH 02/15] ui(mfa): dont require button click to validate mfa, do automatically (#5448) * ui(mfa): dont require link click to validate mfa, do automatically * lint: resolve eslint error level lint checks --- src/dispatch/static/dispatch/src/auth/Mfa.vue | 69 +++++-- .../static/dispatch/src/tests/Mfa.spec.js | 176 ++++++++++++++++++ 2 files changed, 230 insertions(+), 15 deletions(-) create mode 100644 src/dispatch/static/dispatch/src/tests/Mfa.spec.js diff --git a/src/dispatch/static/dispatch/src/auth/Mfa.vue b/src/dispatch/static/dispatch/src/auth/Mfa.vue index c7fd6fc236dd..50f83ca7ec87 100644 --- a/src/dispatch/static/dispatch/src/auth/Mfa.vue +++ b/src/dispatch/static/dispatch/src/auth/Mfa.vue @@ -7,18 +7,32 @@ Multi-Factor Authentication - - {{ statusMessage }} - - - Verify MFA - +
+ + + + + + {{ statusMessage }} + + + + + Retry Verification + +
@@ -27,7 +41,7 @@ diff --git a/src/dispatch/static/dispatch/src/tests/Mfa.spec.js b/src/dispatch/static/dispatch/src/tests/Mfa.spec.js new file mode 100644 index 000000000000..970305938dad --- /dev/null +++ b/src/dispatch/static/dispatch/src/tests/Mfa.spec.js @@ -0,0 +1,176 @@ +import { mount, flushPromises } from "@vue/test-utils" +import { expect, test, vi, beforeEach, afterEach } from "vitest" +import { createVuetify } from "vuetify" +import * as components from "vuetify/components" +import * as directives from "vuetify/directives" +import MfaVerification from "@/auth/mfa.vue" +import authApi from "@/auth/api" + +vi.mock("vue-router", () => ({ + useRoute: () => ({ + query: { + challenge_id: "test-challenge", + project_id: "123", + action: "test-action", + }, + }), +})) + +vi.mock("@/auth/api", () => ({ + default: { + verifyMfa: vi.fn(), + }, +})) + +const vuetify = createVuetify({ + components, + directives, +}) + +global.ResizeObserver = require("resize-observer-polyfill") + +const windowCloseMock = vi.fn() +const originalClose = window.close + +beforeEach(() => { + vi.useFakeTimers() + Object.defineProperty(window, "close", { + value: windowCloseMock, + writable: true, + }) + vi.clearAllMocks() +}) + +afterEach(() => { + vi.useRealTimers() + Object.defineProperty(window, "close", { + value: originalClose, + writable: true, + }) +}) + +test("mounts correctly and starts verification automatically", async () => { + const wrapper = mount(MfaVerification, { + global: { + plugins: [vuetify], + }, + }) + + await flushPromises() + + expect(wrapper.exists()).toBe(true) + expect(authApi.verifyMfa).toHaveBeenCalledWith({ + challenge_id: "test-challenge", + project_id: 123, + action: "test-action", + }) +}) + +test("shows loading state while verifying", async () => { + vi.mocked(authApi.verifyMfa).mockImplementationOnce( + () => new Promise(() => {}) // Never resolving promise + ) + + const wrapper = mount(MfaVerification, { + global: { + plugins: [vuetify], + }, + }) + + await flushPromises() + + const loadingSpinner = wrapper.findComponent({ name: "v-progress-circular" }) + expect(loadingSpinner.exists()).toBe(true) + expect(loadingSpinner.isVisible()).toBe(true) +}) + +test("shows success message and closes window on approval", async () => { + vi.mocked(authApi.verifyMfa).mockResolvedValueOnce({ + data: { status: "approved" }, + }) + + const wrapper = mount(MfaVerification, { + global: { + plugins: [vuetify], + }, + }) + + await flushPromises() + + const alert = wrapper.findComponent({ name: "v-alert" }) + expect(alert.exists()).toBe(true) + expect(alert.props("type")).toBe("success") + expect(alert.text()).toContain("MFA verification successful") + + vi.advanceTimersByTime(5000) + expect(windowCloseMock).toHaveBeenCalled() +}) + +test("shows error message and retry button on denial", async () => { + vi.mocked(authApi.verifyMfa).mockResolvedValueOnce({ + data: { status: "denied" }, + }) + + const wrapper = mount(MfaVerification, { + global: { + plugins: [vuetify], + }, + }) + + await flushPromises() + + const alert = wrapper.findComponent({ name: "v-alert" }) + expect(alert.exists()).toBe(true) + expect(alert.props("type")).toBe("error") + expect(alert.text()).toContain("MFA verification denied") + + const retryButton = wrapper.findComponent({ name: "v-btn" }) + expect(retryButton.exists()).toBe(true) + expect(retryButton.text()).toContain("Retry Verification") +}) + +test("retry button triggers new verification attempt", async () => { + const verifyMfaMock = vi + .mocked(authApi.verifyMfa) + .mockResolvedValueOnce({ + data: { status: "denied" }, + }) + .mockResolvedValueOnce({ + data: { status: "approved" }, + }) + + const wrapper = mount(MfaVerification, { + global: { + plugins: [vuetify], + }, + }) + + await flushPromises() + + const retryButton = wrapper.findComponent({ name: "v-btn" }) + await retryButton.trigger("click") + + await flushPromises() + + expect(verifyMfaMock).toHaveBeenCalledTimes(2) + + const alert = wrapper.findComponent({ name: "v-alert" }) + expect(alert.props("type")).toBe("success") +}) + +test("handles API errors gracefully", async () => { + vi.mocked(authApi.verifyMfa).mockRejectedValueOnce(new Error("API Error")) + + const wrapper = mount(MfaVerification, { + global: { + plugins: [vuetify], + }, + }) + + await flushPromises() + + const alert = wrapper.findComponent({ name: "v-alert" }) + expect(alert.exists()).toBe(true) + expect(alert.props("type")).toBe("error") + expect(alert.text()).toContain("MFA verification denied") +}) From 0ee4db2707ded7ff51ae913e90046f187a6eaf80 Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:19:19 -0800 Subject: [PATCH 03/15] Use local event to prevent slow down (#5444) --- .../dispatch/src/incident/EditEventDialog.vue | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/dispatch/static/dispatch/src/incident/EditEventDialog.vue b/src/dispatch/static/dispatch/src/incident/EditEventDialog.vue index e2887176777b..f1bb197f07c6 100644 --- a/src/dispatch/static/dispatch/src/incident/EditEventDialog.vue +++ b/src/dispatch/static/dispatch/src/incident/EditEventDialog.vue @@ -11,7 +11,7 @@ Cancel - - OK - - OK + OK + OK @@ -79,6 +77,8 @@ export default { timezones: ["UTC", "America/Los_Angeles"], timezone: "UTC", started_at_in_utc: "", + local_started_at: null, + local_description: "", } }, @@ -112,13 +112,24 @@ export default { this.eventStart = new Date() }, update_started_at(val) { - this.started_at = val + this.local_started_at = val this.started_at_in_utc = val }, + updateEvent() { + this.description = this.local_description + this.started_at = this.local_started_at + this.updateExistingEvent() + }, + newEvent() { + this.description = this.local_description + this.started_at = this.local_started_at + this.storeNewEvent() + }, }, mounted() { this.init() - this.started_at_in_utc = this.started_at + this.local_description = this.description + this.local_started_at = this.started_at }, } From 19872d9d11d7d8fb5e63921f41e4b48b96d13903 Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:16:22 -0800 Subject: [PATCH 04/15] Add button to case actions section (#5450) --- src/dispatch/plugins/dispatch_slack/case/messages.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/dispatch/plugins/dispatch_slack/case/messages.py b/src/dispatch/plugins/dispatch_slack/case/messages.py index ae5b7f645a83..2281476d5e4d 100644 --- a/src/dispatch/plugins/dispatch_slack/case/messages.py +++ b/src/dispatch/plugins/dispatch_slack/case/messages.py @@ -275,7 +275,12 @@ def create_action_buttons_message( text="💤 Snooze Alert", action_id=SignalNotificationActions.snooze, value=button_metadata, - ) + ), + Button( + text="👤 User MFA Challenge", + action_id=CaseNotificationActions.user_mfa, + value=button_metadata, + ), ) # we create the signal metadata blocks From 30aea19a0ed66b20ff9bd807aebe74674cdd2c9c Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:29:06 -0800 Subject: [PATCH 05/15] enhancement(ui): allow multiple assignee and participants (#5443) --- .../dispatch/src/case/TableFilterDialog.vue | 17 +++-------------- .../dispatch/src/incident/TableFilterDialog.vue | 16 +++------------- .../static/dispatch/src/router/utils.js | 4 ++++ .../static/dispatch/src/search/utils.js | 7 +++++++ 4 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/dispatch/static/dispatch/src/case/TableFilterDialog.vue b/src/dispatch/static/dispatch/src/case/TableFilterDialog.vue index 6a0f77c80ae0..d7acb07e6c16 100644 --- a/src/dispatch/static/dispatch/src/case/TableFilterDialog.vue +++ b/src/dispatch/static/dispatch/src/case/TableFilterDialog.vue @@ -46,20 +46,19 @@ Case Participant - Show only cases with this participant + Show only cases with these participant(s) @@ -138,19 +137,9 @@ const numFilters = computed(() => { ]) }) -const only_one = (value) => { - if (value && value.length > 1) { - return "Only one is allowed" - } - return true -} - const applyFilters = () => { let filtered_participant = null let filtered_assignee = null - if (Array.isArray(local_participant.value)) { - local_participant.value = local_participant.value[0] - } if (local_participant_is_assignee.value) { filtered_assignee = local_participant.value filtered_participant = null diff --git a/src/dispatch/static/dispatch/src/incident/TableFilterDialog.vue b/src/dispatch/static/dispatch/src/incident/TableFilterDialog.vue index 6c18e08a7469..b246e8217ceb 100644 --- a/src/dispatch/static/dispatch/src/incident/TableFilterDialog.vue +++ b/src/dispatch/static/dispatch/src/incident/TableFilterDialog.vue @@ -66,20 +66,19 @@ Incident Participant - Show only incidents with this participant + Show only incidents with these participant(s) @@ -149,12 +148,6 @@ export default { local_tag_type: [], local_participant_is_commander: false, local_participant: null, - only_one: (value) => { - if (value && value.length > 1) { - return "Only one is allowed" - } - return true - }, } }, @@ -201,9 +194,6 @@ export default { this.tag = this.local_tag this.tag_all = this.local_tag_all this.tag_type = this.local_tag_type - if (Array.isArray(this.local_participant)) { - this.local_participant = this.local_participant[0] - } this.participant = this.local_participant if (this.local_participant_is_commander) { this.commander = this.local_participant diff --git a/src/dispatch/static/dispatch/src/router/utils.js b/src/dispatch/static/dispatch/src/router/utils.js index 6a8989627bed..1cb4f21e4fc6 100644 --- a/src/dispatch/static/dispatch/src/router/utils.js +++ b/src/dispatch/static/dispatch/src/router/utils.js @@ -18,12 +18,16 @@ export default { if (has(flatFilters, key)) { if (typeof item === "string" || item instanceof String) { flatFilters[key].push(item) + } else if (Array.isArray(value)) { + flatFilters[key].push(item.individual.email) } else { flatFilters[key].push(item.email) } } else { if (typeof item === "string" || item instanceof String) { flatFilters[key] = [item] + } else if (Array.isArray(value)) { + flatFilters[key] = [item.individual.email] } else { flatFilters[key] = [item.email] } diff --git a/src/dispatch/static/dispatch/src/search/utils.js b/src/dispatch/static/dispatch/src/search/utils.js index 971e1273f778..70d2cf09dbf8 100644 --- a/src/dispatch/static/dispatch/src/search/utils.js +++ b/src/dispatch/static/dispatch/src/search/utils.js @@ -144,6 +144,13 @@ export default { op: "==", value: value.email, }) + } else if (["commander", "participant", "assignee"].includes(key)) { + subFilter.push({ + model: toPascalCase(key), + field: "email", + op: "==", + value: value.individual.email, + }) } else if (has(value, "id")) { subFilter.push({ model: toPascalCase(key), From 3ab3180b3201fcbb3808dbb9ff2e1872808bce9e Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:46:20 -0800 Subject: [PATCH 06/15] chore(deps): bumping dependencies (#5449) --- requirements-base.in | 1 + requirements-base.txt | 152 +++++++++++++++++------------ requirements-dev.in | 2 +- requirements-dev.txt | 41 ++++---- setup.cfg | 3 - setup.py | 9 +- src/dispatch/conversation/flows.py | 4 + src/dispatch/database/service.py | 5 +- 8 files changed, 125 insertions(+), 92 deletions(-) diff --git a/requirements-base.in b/requirements-base.in index 61bd6fa7d7e6..c6e30e5410ca 100644 --- a/requirements-base.in +++ b/requirements-base.in @@ -32,6 +32,7 @@ pandas pdpyras protobuf<4.24.0,>=3.6.1 psycopg2-binary +pyarrow pydantic==1.* pyparsing python-dateutil diff --git a/requirements-base.txt b/requirements-base.txt index 676cd7b9c3fb..5013aeafedc9 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -4,19 +4,20 @@ # # pip-compile requirements-base.in # + aiocache==0.12.3 # via -r requirements-base.in aiofiles==24.1.0 # via -r requirements-base.in -aiohappyeyeballs==2.3.5 +aiohappyeyeballs==2.4.3 # via aiohttp -aiohttp==3.10.5 +aiohttp==3.10.10 # via -r requirements-base.in aiosignal==1.3.1 # via aiohttp alembic==1.14.0 # via -r requirements-base.in -anyio==4.2.0 +anyio==4.6.2.post1 # via # httpx # openai @@ -33,13 +34,13 @@ backoff==2.2.1 # via schemathesis bcrypt==4.2.0 # via -r requirements-base.in -blis==0.7.11 +blis==1.0.1 # via thinc blockkit==1.5.2 # via -r requirements-base.in -boto3==1.34.13 +boto3==1.35.56 # via -r requirements-base.in -botocore==1.34.13 +botocore==1.35.56 # via # boto3 # s3transfer @@ -53,19 +54,19 @@ catalogue==2.0.10 # spacy # srsly # thinc -certifi==2024.7.4 +certifi==2024.8.30 # via # httpcore # httpx # requests # sentry-sdk -cffi==1.16.0 +cffi==1.17.1 # via cryptography chardet==5.2.0 # via # -r requirements-base.in # emails -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 # via requests click==8.1.7 # via @@ -73,11 +74,11 @@ click==8.1.7 # schemathesis # typer # uvicorn -cloudpathlib==0.16.0 +cloudpathlib==0.20.0 # via weasel colorama==0.4.6 # via schemathesis -confection==0.1.4 +confection==0.1.5 # via # thinc # weasel @@ -89,7 +90,7 @@ cryptography==39.0.2 # pyjwt cssselect==1.2.0 # via premailer -cssutils==2.9.0 +cssutils==2.11.1 # via # emails # premailer @@ -108,27 +109,27 @@ deprecated==1.2.14 # limits distro==1.9.0 # via openai -dnspython==2.6.1 +dnspython==2.7.0 # via email-validator duo-client==5.3.0 # via -r requirements-base.in -ecdsa==0.18.0 +ecdsa==0.19.0 # via python-jose email-validator==2.2.0 # via -r requirements-base.in emails==0.6 # via -r requirements-base.in -fastapi==0.108.0 +fastapi==0.115.4 # via -r requirements-base.in -frozenlist==1.4.1 +frozenlist==1.5.0 # via # aiohttp # aiosignal -google-api-core==2.15.0 +google-api-core==2.22.0 # via google-api-python-client google-api-python-client==2.151.0 # via -r requirements-base.in -google-auth==2.26.1 +google-auth==2.36.0 # via # google-api-core # google-api-python-client @@ -138,16 +139,16 @@ google-auth-httplib2==0.2.0 # via google-api-python-client google-auth-oauthlib==1.2.1 # via -r requirements-base.in -googleapis-common-protos==1.62.0 +googleapis-common-protos==1.65.0 # via google-api-core -graphql-core==3.2.3 +graphql-core==3.2.5 # via hypothesis-graphql h11==0.14.0 # via # -r requirements-base.in # httpcore # uvicorn -httpcore==1.0.2 +httpcore==1.0.6 # via httpx httplib2==0.22.0 # via @@ -164,18 +165,18 @@ hypothesis==6.91.0 # hypothesis-graphql # hypothesis-jsonschema # schemathesis -hypothesis-graphql==0.11.0 +hypothesis-graphql==0.11.1 # via schemathesis hypothesis-jsonschema==0.22.1 # via schemathesis -idna==3.7 +idna==3.10 # via # anyio # email-validator # httpx # requests # yarl -importlib-resources==6.1.1 +importlib-resources==6.4.5 # via limits iniconfig==2.0.0 # via pytest @@ -185,6 +186,8 @@ jinja2==3.1.4 # spacy jira==2.0.0 # via -r requirements-base.in +jiter==0.7.0 + # via openai jmespath==1.0.1 # via # boto3 @@ -199,26 +202,36 @@ jsonschema==4.17.3 # schemathesis junit-xml==1.9 # via schemathesis -langcodes==3.3.0 +langcodes==3.4.1 # via spacy -limits==3.7.0 +language-data==1.2.0 + # via langcodes +limits==3.13.0 # via slowapi -lxml==5.0.0 +lxml==5.3.0 # via # emails # premailer -mako==1.3.0 +mako==1.3.6 # via alembic +marisa-trie==1.2.1 + # via language-data markdown==3.7 # via -r requirements-base.in -markupsafe==2.1.3 +markdown-it-py==3.0.0 + # via rich +markupsafe==3.0.2 # via # jinja2 # mako # werkzeug +mdurl==0.1.2 + # via markdown-it-py +more-itertools==10.5.0 + # via cssutils msal==1.31.0 # via -r requirements-base.in -multidict==6.0.4 +multidict==6.1.0 # via # aiohttp # yarl @@ -227,7 +240,7 @@ murmurhash==1.0.10 # preshed # spacy # thinc -numpy==1.26.4 +numpy==2.0.2 # via # -r requirements-base.in # blis @@ -244,9 +257,9 @@ oauthlib[signedtoken]==3.2.2 # atlassian-python-api # jira # requests-oauthlib -openai==1.39.0 +openai==1.54.3 # via -r requirements-base.in -packaging==23.2 +packaging==24.2 # via # limits # pytest @@ -260,11 +273,11 @@ pandas==2.2.3 # statsmodels patsy==0.5.6 # via statsmodels -pbr==6.0.0 +pbr==6.1.0 # via jira pdpyras==5.3.0 # via -r requirements-base.in -pluggy==1.3.0 +pluggy==1.5.0 # via pytest ply==3.11 # via jsonpath-ng @@ -274,26 +287,33 @@ preshed==3.0.9 # via # spacy # thinc +propcache==0.2.0 + # via yarl +proto-plus==1.25.0 + # via google-api-core protobuf==4.23.4 # via # -r requirements-base.in # google-api-core # googleapis-common-protos + # proto-plus psycopg2-binary==2.9.10 # via -r requirements-base.in -pyasn1==0.5.1 +pyarrow==18.0.0 + # via -r requirements-base.in +pyasn1==0.6.1 # via # oauth2client # pyasn1-modules # python-jose # rsa -pyasn1-modules==0.3.0 +pyasn1-modules==0.4.1 # via # google-auth # oauth2client -pycparser==2.21 +pycparser==2.22 # via cffi -pydantic==1.10.18 +pydantic==1.10.19 # via # -r requirements-base.in # blockkit @@ -303,7 +323,9 @@ pydantic==1.10.18 # spacy # thinc # weasel -pyjwt[crypto]==2.8.0 +pygments==2.18.0 + # via rich +pyjwt[crypto]==2.9.0 # via # msal # oauthlib @@ -338,7 +360,7 @@ pytz==2024.2 # via # -r requirements-base.in # pandas -pyyaml==6.0.1 +pyyaml==6.0.2 # via schemathesis requests==2.32.3 # via @@ -356,34 +378,38 @@ requests==2.32.3 # spacy # starlette-testclient # weasel -requests-oauthlib==1.3.1 +requests-oauthlib==2.0.0 # via # atlassian-python-api # google-auth-oauthlib # jira requests-toolbelt==1.0.0 # via jira +rich==13.9.4 + # via typer rsa==4.9 # via # google-auth # oauth2client # python-jose -s3transfer==0.10.0 +s3transfer==0.10.3 # via boto3 schedule==1.2.2 # via -r requirements-base.in schemathesis==3.21.2 # via -r requirements-base.in -scipy==1.11.4 +scipy==1.14.1 # via statsmodels sentry-asgi==0.2.0 # via -r requirements-base.in -sentry-sdk==1.45.0 +sentry-sdk==2.18.0 # via # -r requirements-base.in # sentry-asgi sh==2.1.0 # via -r requirements-base.in +shellingham==1.5.4 + # via typer six==1.16.0 # via # atlassian-python-api @@ -404,16 +430,16 @@ slack-sdk==3.33.3 # slack-bolt slowapi==0.1.9 # via -r requirements-base.in -smart-open==6.4.0 +smart-open==7.0.5 # via weasel -sniffio==1.3.0 +sniffio==1.3.1 # via # anyio # httpx # openai sortedcontainers==2.4.0 # via hypothesis -spacy==3.8.0 +spacy==3.8.2 # via -r requirements-base.in spacy-legacy==3.0.12 # via spacy @@ -435,7 +461,7 @@ srsly==2.4.8 # spacy # thinc # weasel -starlette==0.32.0.post1 +starlette==0.41.2 # via # fastapi # schemathesis @@ -450,21 +476,21 @@ tenacity==9.0.0 # via -r requirements-base.in text-unidecode==1.3 # via python-slugify -thinc==8.2.2 +thinc==8.3.2 # via spacy -tomli==2.0.1 +tomli==2.0.2 # via schemathesis -tomli-w==1.0.0 +tomli-w==1.1.0 # via schemathesis -tqdm==4.66.4 +tqdm==4.67.0 # via # openai # spacy -typer==0.9.0 +typer==0.13.0 # via # spacy # weasel -typing-extensions==4.10.0 +typing-extensions==4.12.2 # via # alembic # fastapi @@ -473,11 +499,11 @@ typing-extensions==4.10.0 # pydantic # schemathesis # typer -tzdata==2023.4 +tzdata==2024.2 # via pandas uritemplate==4.1.1 # via google-api-python-client -urllib3==2.0.7 +urllib3==2.2.3 # via # botocore # pdpyras @@ -489,18 +515,20 @@ uvloop==0.21.0 # via -r requirements-base.in validators==0.18.2 # via -r requirements-base.in -wasabi==1.1.2 +wasabi==1.1.3 # via # spacy # thinc # weasel -weasel==0.3.4 +weasel==0.4.1 # via spacy -werkzeug==3.0.6 +werkzeug==3.1.3 # via schemathesis wrapt==1.16.0 - # via deprecated -yarl==1.9.4 + # via + # deprecated + # smart-open +yarl==1.17.1 # via # aiohttp # schemathesis diff --git a/requirements-dev.in b/requirements-dev.in index c9d74c103eee..43f459d08a6a 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -7,6 +7,6 @@ factory-boy faker ipython pre-commit -pytest +pytest==7.4.4 ruff vulture diff --git a/requirements-dev.txt b/requirements-dev.txt index 51cfd950fd80..2b93043d02da 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,9 +24,9 @@ decorator==5.1.1 # via ipython devtools==0.12.2 # via -r requirements-dev.in -distlib==0.3.8 +distlib==0.3.9 # via virtualenv -executing==2.0.1 +executing==2.1.0 # via # devtools # stack-data @@ -36,9 +36,9 @@ faker==30.8.2 # via # -r requirements-dev.in # factory-boy -filelock==3.13.1 +filelock==3.16.1 # via virtualenv -identify==2.5.33 +identify==2.6.1 # via pre-commit iniconfig==2.0.0 # via pytest @@ -46,37 +46,37 @@ ipython==8.29.0 # via -r requirements-dev.in jedi==0.19.1 # via ipython -matplotlib-inline==0.1.6 +matplotlib-inline==0.1.7 # via ipython mypy-extensions==1.0.0 # via black -nodeenv==1.8.0 +nodeenv==1.9.1 # via pre-commit -packaging==23.2 +packaging==24.2 # via # black # pytest -parso==0.8.3 +parso==0.8.4 # via jedi pathspec==0.12.1 # via black pexpect==4.9.0 # via ipython -platformdirs==4.1.0 +platformdirs==4.3.6 # via # black # virtualenv -pluggy==1.3.0 +pluggy==1.5.0 # via pytest pre-commit==4.0.1 # via -r requirements-dev.in -prompt-toolkit==3.0.43 +prompt-toolkit==3.0.48 # via ipython ptyprocess==0.7.0 # via pexpect -pure-eval==0.2.2 +pure-eval==0.2.3 # via stack-data -pygments==2.17.2 +pygments==2.18.0 # via # devtools # ipython @@ -84,9 +84,9 @@ pytest==7.4.4 # via -r requirements-dev.in python-dateutil==2.9.0.post0 # via faker -pyyaml==6.0.1 +pyyaml==6.0.2 # via pre-commit -ruff==0.7.2 +ruff==0.7.3 # via -r requirements-dev.in six==1.16.0 # via @@ -94,20 +94,17 @@ six==1.16.0 # python-dateutil stack-data==0.6.3 # via ipython -traitlets==5.14.1 +traitlets==5.14.3 # via # ipython # matplotlib-inline -typing-extensions==4.10.0 +typing-extensions==4.12.2 # via # faker # ipython -virtualenv==20.25.0 +virtualenv==20.27.1 # via pre-commit vulture==2.13 # via -r requirements-dev.in -wcwidth==0.2.12 +wcwidth==0.2.13 # via prompt-toolkit - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/setup.cfg b/setup.cfg index ccab80351c6b..38f2ded19757 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,9 +6,6 @@ looponfailroots = src tests selenium_driver = chrome self-contained-html = true -[bdist_wheel] -python-tag = py38 - [coverage:run] omit = dispatch/migrations/* diff --git a/setup.py b/setup.py index 169f24975aa8..f297a169ceac 100644 --- a/setup.py +++ b/setup.py @@ -338,7 +338,13 @@ def get_asset_json_path(self): def get_requirements(env): with open("requirements-{}.txt".format(env)) as fp: - return [x.strip() for x in fp.read().split("\n") if not x.startswith("#")] + return [ + x.strip() + for x in fp.read().split("\n") + if not x.strip().startswith("#") + and not x.strip().startswith("--") + and not x.strip() == "" + ] install_requires = get_requirements("base") @@ -406,7 +412,6 @@ def run(self): "dispatch_auth_mfa = dispatch.plugins.dispatch_core.plugin:DispatchMfaPlugin", "dispatch_basic_auth = dispatch.plugins.dispatch_core.plugin:BasicAuthProviderPlugin", "dispatch_contact = dispatch.plugins.dispatch_core.plugin:DispatchContactPlugin", - "dispatch_document_resolver = dispatch.plugins.dispatch_core.plugin:DispatchDocumentResolverPlugin", "dispatch_header_auth = dispatch.plugins.dispatch_core.plugin:HeaderAuthProviderPlugin", "dispatch_participant_resolver = dispatch.plugins.dispatch_core.plugin:DispatchParticipantResolverPlugin", "dispatch_pkce_auth = dispatch.plugins.dispatch_core.plugin:PKCEAuthProviderPlugin", diff --git a/src/dispatch/conversation/flows.py b/src/dispatch/conversation/flows.py index 375450312f3e..08eefe7dcd24 100644 --- a/src/dispatch/conversation/flows.py +++ b/src/dispatch/conversation/flows.py @@ -399,6 +399,10 @@ def add_conversation_bookmark( title: str | None = None, ): """Adds a conversation bookmark.""" + if not resource or not hasattr(resource, "name"): + log.warning("No conversation bookmark added since no resource available for subject.") + return + if not subject.conversation: log.warning( f"Conversation bookmark {resource.name.lower()} not added. No conversation available." diff --git a/src/dispatch/database/service.py b/src/dispatch/database/service.py index 209a3735b86f..39dd25144cd0 100644 --- a/src/dispatch/database/service.py +++ b/src/dispatch/database/service.py @@ -477,7 +477,7 @@ def common_parameters( page: int = Query(1, gt=0, lt=2147483647), items_per_page: int = Query(5, alias="itemsPerPage", gt=-2, lt=2147483647), query_str: QueryStr = Query(None, alias="q"), - filter_spec: Json = Query([], alias="filter"), + filter_spec: QueryStr = Query(None, alias="filter"), sort_by: List[str] = Query([], alias="sortBy[]"), descending: List[bool] = Query([], alias="descending[]"), role: UserRoles = Depends(get_current_role), @@ -536,7 +536,7 @@ def search_filter_sort_paginate( db_session, model, query_str: str = None, - filter_spec: List[dict] = None, + filter_spec: str = None, page: int = 1, items_per_page: int = 5, sort_by: List[str] = None, @@ -558,6 +558,7 @@ def search_filter_sort_paginate( tag_all_filters = [] if filter_spec: + filter_spec = json.loads(filter_spec) query = apply_filter_specific_joins(model_cls, filter_spec, query) # if the filter_spec has the TagAll filter, we need to split the query up # and intersect all of the results From 83f368eb4f73866d86335c460b54d00b375e8984 Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:52:41 -0800 Subject: [PATCH 07/15] Cleaning up alembic migration files (#5431) --- .../versions/2024-11-04_928b725d64f6.py | 50 +++++++++++++++++++ src/dispatch/signal/models.py | 13 ----- 2 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 src/dispatch/database/revisions/tenant/versions/2024-11-04_928b725d64f6.py diff --git a/src/dispatch/database/revisions/tenant/versions/2024-11-04_928b725d64f6.py b/src/dispatch/database/revisions/tenant/versions/2024-11-04_928b725d64f6.py new file mode 100644 index 000000000000..429f311fbcab --- /dev/null +++ b/src/dispatch/database/revisions/tenant/versions/2024-11-04_928b725d64f6.py @@ -0,0 +1,50 @@ +"""Fixes automatic generation issues + +Revision ID: 928b725d64f6 +Revises: 3edb0476365a +Create Date: 2024-11-04 15:55:57.864691 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.engine.reflection import Inspector + +# revision identifiers, used by Alembic. +revision = "928b725d64f6" +down_revision = "3edb0476365a" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + conn = op.get_bind() + inspector = Inspector.from_engine(conn) + + # Check if the table exists + if "service_incident" in inspector.get_table_names(): + op.drop_table("service_incident") + + op.alter_column( + "entity", "source", existing_type=sa.BOOLEAN(), type_=sa.String(), existing_nullable=True + ) + + op.drop_index("ix_entity_search_vector", table_name="entity", postgresql_using="gin") + op.create_index( + "entity_search_vector_idx", + "entity", + ["search_vector"], + unique=False, + postgresql_using="gin", + ) + op.alter_column("entity_type", "jpath", existing_type=sa.VARCHAR(), nullable=True) + op.drop_column("plugin_instance", "configuration") + op.drop_constraint("project_stable_priority_id_fkey", "project", type_="foreignkey") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/src/dispatch/signal/models.py b/src/dispatch/signal/models.py index a0fc51e5ce8d..a41177165595 100644 --- a/src/dispatch/signal/models.py +++ b/src/dispatch/signal/models.py @@ -48,19 +48,6 @@ class RuleMode(DispatchEnum): inactive = "Inactive" -assoc_signal_instance_tags = Table( - "assoc_signal_instance_tags", - Base.metadata, - Column( - "signal_instance_id", - UUID(as_uuid=True), - ForeignKey("signal_instance.id", ondelete="CASCADE"), - ), - Column("tag_id", Integer, ForeignKey("tag.id", ondelete="CASCADE")), - PrimaryKeyConstraint("signal_instance_id", "tag_id"), -) - - assoc_signal_tags = Table( "assoc_signal_tags", Base.metadata, From 8109845025ccb913f2929623610e29fa65dadf1f Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:40:20 -0800 Subject: [PATCH 08/15] Ensure sentry_sdk is pinned at 1.45.0 (#5455) --- requirements-base.in | 2 +- requirements-base.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-base.in b/requirements-base.in index c6e30e5410ca..ee63da82d474 100644 --- a/requirements-base.in +++ b/requirements-base.in @@ -44,7 +44,7 @@ requests schedule schemathesis sentry-asgi -sentry-sdk +sentry-sdk==1.45.0 sh slack_sdk slack-bolt diff --git a/requirements-base.txt b/requirements-base.txt index 5013aeafedc9..94303f8685f8 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -402,7 +402,7 @@ scipy==1.14.1 # via statsmodels sentry-asgi==0.2.0 # via -r requirements-base.in -sentry-sdk==2.18.0 +sentry-sdk==1.45.0 # via # -r requirements-base.in # sentry-asgi From 6514110cc2cae6a56d0b4d0d249002d29764079f Mon Sep 17 00:00:00 2001 From: David Whittaker <84562015+whitdog47@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:40:51 -0800 Subject: [PATCH 09/15] Fixes migration to ensure index and column exist before dropping (#5456) --- .../versions/2024-11-04_928b725d64f6.py | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/dispatch/database/revisions/tenant/versions/2024-11-04_928b725d64f6.py b/src/dispatch/database/revisions/tenant/versions/2024-11-04_928b725d64f6.py index 429f311fbcab..72a8a2829b82 100644 --- a/src/dispatch/database/revisions/tenant/versions/2024-11-04_928b725d64f6.py +++ b/src/dispatch/database/revisions/tenant/versions/2024-11-04_928b725d64f6.py @@ -30,16 +30,28 @@ def upgrade(): "entity", "source", existing_type=sa.BOOLEAN(), type_=sa.String(), existing_nullable=True ) - op.drop_index("ix_entity_search_vector", table_name="entity", postgresql_using="gin") - op.create_index( - "entity_search_vector_idx", - "entity", - ["search_vector"], - unique=False, - postgresql_using="gin", - ) + indexes = inspector.get_indexes("entity") + index_exists = any(index["name"] == "ix_entity_search_vector" for index in indexes) + + if index_exists: + op.drop_index("ix_entity_search_vector", table_name="entity", postgresql_using="gin") + + index_exists = any(index["name"] == "entity_search_vector_idx" for index in indexes) + if not index_exists: + op.create_index( + "entity_search_vector_idx", + "entity", + ["search_vector"], + unique=False, + postgresql_using="gin", + ) op.alter_column("entity_type", "jpath", existing_type=sa.VARCHAR(), nullable=True) - op.drop_column("plugin_instance", "configuration") + + columns = inspector.get_columns("plugin_instance") + column_exists = any(column["name"] == "configuration" for column in columns) + if column_exists: + op.drop_column("plugin_instance", "configuration") + op.drop_constraint("project_stable_priority_id_fkey", "project", type_="foreignkey") # ### end Alembic commands ### From 1792378b4441e7e84305f50be5527ab8b67d9ca1 Mon Sep 17 00:00:00 2001 From: Will Sheldon <114631109+wssheldon@users.noreply.github.com> Date: Tue, 12 Nov 2024 17:03:12 -0800 Subject: [PATCH 10/15] feat(ui): add dedicated status tab for can report in incident edit sheet (#5459) * feat(ui): add dedicated status tab for can report in incident edit sheet * ui: change status to reports * tests: add component tests for timeline report tab --- .../dispatch/src/incident/EditSheet.vue | 6 + .../src/incident/TimelineReportTab.vue | 73 ++++++++ .../src/tests/TimelineReportTab.spec.js | 163 ++++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 src/dispatch/static/dispatch/src/incident/TimelineReportTab.vue create mode 100644 src/dispatch/static/dispatch/src/tests/TimelineReportTab.spec.js diff --git a/src/dispatch/static/dispatch/src/incident/EditSheet.vue b/src/dispatch/static/dispatch/src/incident/EditSheet.vue index 1ba003c26165..ec7239ec69f1 100644 --- a/src/dispatch/static/dispatch/src/incident/EditSheet.vue +++ b/src/dispatch/static/dispatch/src/incident/EditSheet.vue @@ -34,6 +34,7 @@ Details + Reports Resources Participants Timeline @@ -46,6 +47,9 @@ + + + @@ -81,6 +85,7 @@ import IncidentCostsTab from "@/incident/CostsTab.vue" import IncidentDetailsTab from "@/incident/DetailsTab.vue" import IncidentParticipantsTab from "@/incident/ParticipantsTab.vue" import IncidentResourcesTab from "@/incident/ResourcesTab.vue" +import IncidentTimelineReportTab from "@/incident/TimelineReportTab.vue" import IncidentTasksTab from "@/incident/TasksTab.vue" import IncidentTimelineTab from "@/incident/TimelineTab.vue" import WorkflowInstanceTab from "@/workflow/WorkflowInstanceTab.vue" @@ -94,6 +99,7 @@ export default { IncidentDetailsTab, IncidentParticipantsTab, IncidentResourcesTab, + IncidentTimelineReportTab, IncidentTasksTab, IncidentTimelineTab, WorkflowInstanceTab, diff --git a/src/dispatch/static/dispatch/src/incident/TimelineReportTab.vue b/src/dispatch/static/dispatch/src/incident/TimelineReportTab.vue new file mode 100644 index 000000000000..0518f1480ef0 --- /dev/null +++ b/src/dispatch/static/dispatch/src/incident/TimelineReportTab.vue @@ -0,0 +1,73 @@ + + + + +