Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes signals ingestion from opt in to opt out approach #4396

Merged
merged 13 commits into from
Feb 16, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Adds boolean column default to signal model

Revision ID: 0283f2bbe9dd
Revises: d4bbb234d0bc
Create Date: 2024-02-13 15:31:17.975089

"""

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "0283f2bbe9dd"
down_revision = "d4bbb234d0bc"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("signal", sa.Column("default", sa.Boolean(), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("signal", "default")
# ### end Alembic commands ###
21 changes: 12 additions & 9 deletions src/dispatch/signal/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ class Signal(Base, TimeStampMixin, ProjectMixin):
case_priority = relationship("CasePriority", backref="signals")
create_case = Column(Boolean, default=True)
conversation_target = Column(String)
default = Column(Boolean, default=False)

oncall_service_id = Column(Integer, ForeignKey("service.id"))
oncall_service = relationship("Service", foreign_keys=[oncall_service_id])
engagements = relationship(
Expand Down Expand Up @@ -293,21 +295,22 @@ class SignalFilterPagination(Pagination):


class SignalBase(DispatchBase):
name: str
owner: str
case_priority: Optional[CasePriorityRead]
case_type: Optional[CaseTypeRead]
conversation_target: Optional[str]
create_case: Optional[bool] = True
created_at: Optional[datetime] = None
default: Optional[bool] = False
description: Optional[str]
variant: Optional[str]
case_type: Optional[CaseTypeRead]
case_priority: Optional[CasePriorityRead]
external_id: str
enabled: Optional[bool] = False
external_id: str
external_url: Optional[str]
create_case: Optional[bool] = True
name: str
oncall_service: Optional[Service]
source: Optional[SourceBase]
created_at: Optional[datetime] = None
owner: str
project: ProjectRead
source: Optional[SourceBase]
variant: Optional[str]


class SignalCreate(SignalBase):
Expand Down
80 changes: 52 additions & 28 deletions src/dispatch/signal/service.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
import logging
import json
from datetime import datetime, timedelta, timezone
from typing import Optional, Union

from pydantic.error_wrappers import ErrorWrapper, ValidationError

from sqlalchemy import desc, asc, or_
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import true

from dispatch.auth.models import DispatchUser
from dispatch.case.priority import service as case_priority_service
from dispatch.case.type import service as case_type_service
from dispatch.case.type.models import CaseType
from dispatch.database.service import apply_filter_specific_joins, apply_filters
from dispatch.entity import service as entity_service
from dispatch.entity.models import Entity
from dispatch.entity_type import service as entity_type_service
from dispatch.entity_type.models import EntityScopeEnum
from dispatch.entity_type.models import EntityType
from dispatch.exceptions import NotFoundError
from dispatch.project import service as project_service
from dispatch.service import service as service_service
from dispatch.tag import service as tag_service
from dispatch.workflow import service as workflow_service
from dispatch.entity.models import Entity
from sqlalchemy.exc import IntegrityError
from dispatch.entity_type.models import EntityScopeEnum
from dispatch.entity import service as entity_service

from .exceptions import (
SignalNotDefinedException,
SignalNotEnabledException,
SignalNotIdentifiedException,
)

Expand All @@ -47,6 +49,8 @@
SignalUpdate,
)

log = logging.getLogger(__name__)


def create_signal_engagement(
*, db_session: Session, creator: DispatchUser, signal_engagement_in: SignalEngagementCreate
Expand Down Expand Up @@ -100,7 +104,7 @@ def get_all_by_entity_type(*, db_session: Session, entity_type_id: int) -> list[
def get_signal_engagement_by_name(
*, db_session, project_id: int, name: str
) -> Optional[SignalEngagement]:
"""Gets a signal engagement by it's name."""
"""Gets a signal engagement by its name."""
return (
db_session.query(SignalEngagement)
.filter(SignalEngagement.project_id == project_id)
Expand Down Expand Up @@ -133,32 +137,43 @@ def get_signal_engagement_by_name_or_raise(


def create_signal_instance(*, db_session: Session, signal_instance_in: SignalInstanceCreate):
"""Creates a new signal instance."""
project = project_service.get_by_name_or_default(
db_session=db_session, project_in=signal_instance_in.project
)

if not signal_instance_in.signal:
external_id = signal_instance_in.external_id

# this assumes the external_ids are uuids
if external_id:
signal = (
db_session.query(Signal).filter(Signal.external_id == external_id).one_or_none()
)
signal_instance_in.signal = signal
else:
msg = "An externalId must be provided."
if not external_id:
msg = "A detection external id must be provided in order to get the signal definition."
raise SignalNotIdentifiedException(msg)

if not signal:
msg = f"No signal definition found. ExternalId: {external_id}"
signal_definition = (
db_session.query(Signal).filter(Signal.external_id == external_id).one_or_none()
)

if not signal_definition:
# we get the default signal definition
signal_definition = get_default(
db_session=db_session,
project_id=project.id,
)
msg = "Default signal definition used for signal instance with external id {external_id}"
log.warn(msg)

if not signal_definition:
msg = f"No signal definition could be found by external id {external_id}, and no default exists."
raise SignalNotDefinedException(msg)

if not signal.enabled:
msg = f"Signal definition not enabled. SignalName: {signal.name} ExternalId: {signal.external_id}"
raise SignalNotEnabledException(msg)
signal_instance_in.signal = signal_definition

try:
signal_instance = create_instance(
db_session=db_session, signal_instance_in=signal_instance_in
)
signal_instance.signal = signal
signal_instance.signal = signal_definition
db_session.commit()
except IntegrityError:
db_session.rollback()
Expand Down Expand Up @@ -257,7 +272,7 @@ def get_signal_filter_by_name_or_raise(


def get_signal_filter_by_name(*, db_session, project_id: int, name: str) -> Optional[SignalFilter]:
"""Gets a signal filter by it's name."""
"""Gets a signal filter by its name."""
return (
db_session.query(SignalFilter)
.filter(SignalFilter.project_id == project_id)
Expand All @@ -274,7 +289,7 @@ def get_signal_filter(*, db_session: Session, signal_filter_id: int) -> SignalFi
def get_signal_instance(
*, db_session: Session, signal_instance_id: int | str
) -> Optional[SignalInstance]:
"""Gets a signal instance by it's UUID."""
"""Gets a signal instance by its UUID."""
return (
db_session.query(SignalInstance)
.filter(SignalInstance.id == signal_instance_id)
Expand All @@ -283,10 +298,19 @@ def get_signal_instance(


def get(*, db_session: Session, signal_id: Union[str, int]) -> Optional[Signal]:
"""Gets a signal by id or external_id."""
"""Gets a signal by id."""
return db_session.query(Signal).filter(Signal.id == signal_id).one_or_none()


def get_default(*, db_session: Session, project_id: int) -> Optional[Signal]:
"""Gets the default signal definition."""
return (
db_session.query(Signal)
.filter(Signal.project_id == project_id, Signal.default == true())
.one_or_none()
)


def get_by_primary_or_external_id(
*, db_session: Session, signal_id: Union[str, int]
) -> Optional[Signal]:
Expand All @@ -302,7 +326,7 @@ def get_by_primary_or_external_id(
def get_by_variant_or_external_id(
*, db_session: Session, project_id: int, external_id: str = None, variant: str = None
) -> Optional[Signal]:
"""Gets a signal it's external id (and variant if supplied)."""
"""Gets a signal by its variant or external id."""
if variant:
return (
db_session.query(Signal)
Expand All @@ -319,7 +343,7 @@ def get_by_variant_or_external_id(
def get_all_by_conversation_target(
*, db_session: Session, project_id: int, conversation_target: str
) -> list[Signal]:
"""Gets all signals for a given conversation target. (e.g. #conversation-channel)"""
"""Gets all signals for a given conversation target (e.g. #conversation-channel)"""
return (
db_session.query(Signal)
.join(CaseType)
Expand All @@ -341,14 +365,14 @@ def create(*, db_session: Session, signal_in: SignalCreate) -> Signal:
signal = Signal(
**signal_in.dict(
exclude={
"project",
"case_type",
"case_priority",
"source",
"filters",
"tags",
"case_type",
"entity_types",
"filters",
"oncall_service",
"project",
"source",
"tags",
"workflows",
}
),
Expand Down
46 changes: 20 additions & 26 deletions src/dispatch/signal/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, Response, status, Depends
from pydantic.error_wrappers import ErrorWrapper, ValidationError

from sqlalchemy.exc import IntegrityError

from dispatch.auth.permissions import SensitiveProjectActionPermission, PermissionsDependency
Expand Down Expand Up @@ -73,47 +74,40 @@ def create_signal_instance(
)

if not signal_instance_in.signal:
# we try to get the signal definition by external id or variant
external_id = signal_instance_in.raw.get("externalId")
variant = signal_instance_in.raw.get("variant")
signal_definition = signal_service.get_by_variant_or_external_id(
db_session=db_session,
project_id=project.id,
external_id=external_id,
variant=variant,
)

if external_id or variant:
signal = signal_service.get_by_variant_or_external_id(
db_session=db_session,
project_id=project.id,
external_id=external_id,
variant=variant,
)

signal_instance_in.signal = signal
else:
msg = "An external id or variant must be provided."
log.warn(msg)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=[{"msg": msg}],
) from None
if not signal_definition:
# we get the default signal definition
signal_definition = signal_service.get_default(
db_session=db_session,
project_id=project.id,
)
msg = "Default signal definition used for signal instance with external id {external_id} or variant {variant}."
mvilanova marked this conversation as resolved.
Show resolved Hide resolved
log.warn(msg)

if not signal:
msg = f"No signal definition found. External Id: {external_id} Variant: {variant}"
if not signal_definition:
msg = f"No signal definition could be found by external id {external_id} or variant {variant}, and no default exists."
log.warn(msg)
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=[{"msg": msg}],
) from None

if not signal.enabled:
msg = f"Signal definition not enabled. Signal Name: {signal.name}"
log.info(msg)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=[{"msg": msg}],
) from None
signal_instance_in.signal = signal_definition

try:
signal_instance = signal_service.create_instance(
db_session=db_session, signal_instance_in=signal_instance_in
)
signal_instance.signal = signal
signal_instance.signal = signal_definition
mvilanova marked this conversation as resolved.
Show resolved Hide resolved
db_session.commit()
except IntegrityError:
db_session.rollback()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
label="Priority"
return-object
:loading="loading"
:error-messages="show_error"
:rules="[is_priority_in_project]"
clearable
>
<template #item="data">
<v-list-item v-bind="data.props" :title="null">
Expand Down Expand Up @@ -63,12 +65,27 @@ export default {
this.validatePriority()
},
},
show_error() {
let items_names = this.items.map((item) => item.name)
let selected_item = this.case_priority?.name || ""
if (items_names.includes(selected_item) || selected_item == "") {
return null
}
return "Not a valid case priority"
},
},

methods: {
validatePriority() {
const project_id = this.project?.id || 0
const in_project = this.case_priority?.project?.id == project_id
let in_project
if (this.project?.name) {
let project_name = this.project?.name || ""
in_project = this.case_priority?.project?.name == project_name
} else {
let project_id = this.project?.id || 0
in_project = this.case_priority?.project?.id == project_id
}

if (in_project) {
this.error = true
} else {
Expand Down
Loading
Loading