Example #1
0
    async def editStreetsResource(self, request: IRequest) -> KleinRenderable:
        """
        Street list edit endpoint.
        """
        await self.config.authProvider.authorizeRequest(
            request, None, Authorization.imsAdmin
        )

        store = self.config.store

        try:
            edits = objectFromJSONBytesIO(request.content)
        except JSONDecodeError as e:
            return invalidJSONResponse(request, e)

        for eventID, _streets in edits.items():
            event = Event(id=eventID)
            existing = await store.concentricStreets(event)

            for _streetID, _streetName in existing.items():
                raise NotAuthorizedError("Removal of streets is not allowed.")

        for eventID, streets in edits.items():
            event = Event(id=eventID)
            existing = await store.concentricStreets(event)

            for streetID, streetName in streets.items():
                if streetID not in existing:
                    await store.createConcentricStreet(
                        event, streetID, streetName
                    )

        return noContentResponse(request)
Example #2
0
    async def test_events(self, broken: bool = False) -> None:
        """
        :meth:`IMSDataStore.events` returns all events.
        """
        store = await self.store()

        for event in (Event(id="Event A"), Event(id="Event B")):
            await store.storeEvent(event)

        if broken:
            store.bringThePain()

        events = frozenset(await store.events())

        self.assertEqual(events, {Event(id="Event A"), Event(id="Event B")})
Example #3
0
    async def editAdminAccessResource(
        self, request: IRequest
    ) -> KleinRenderable:
        """
        Admin access control edit endpoint.
        """
        await self.config.authProvider.authorizeRequest(
            request, None, Authorization.imsAdmin
        )

        store = self.config.store

        try:
            edits = objectFromJSONBytesIO(request.content)
        except JSONDecodeError as e:
            return invalidJSONResponse(request, e)

        for eventID, acl in edits.items():
            event = Event(id=eventID)
            if "readers" in acl:
                await store.setReaders(event, acl["readers"])
            if "writers" in acl:
                await store.setWriters(event, acl["writers"])
            if "reporters" in acl:
                await store.setReporters(event, acl["reporters"])

        return noContentResponse(request)
Example #4
0
    async def editEventsResource(self, request: IRequest) -> KleinRenderable:
        """
        Events editing endpoint.
        """
        await self.config.authProvider.authorizeRequest(
            request, None, Authorization.imsAdmin
        )

        try:
            json = objectFromJSONBytesIO(request.content)
        except JSONDecodeError as e:
            return invalidJSONResponse(request, e)

        if type(json) is not dict:
            self._log.debug(
                "Events update expected a dictionary, got {json!r}", json=json
            )
            return badRequestResponse(request, "root: expected a dictionary.")

        adds = json.get("add", [])

        store = self.config.store

        if adds:
            if type(adds) is not list:
                self._log.debug(
                    "Events add expected a list, got {adds!r}",
                    json=json,
                    adds=adds,
                )
                return badRequestResponse(request, "add: expected a list.")
            for eventID in adds:
                await store.createEvent(Event(id=eventID))

        return noContentResponse(request)
Example #5
0
    async def readIncidentResource(
        self, request: IRequest, eventID: str, number: str
    ) -> KleinRenderable:
        """
        Incident endpoint.
        """
        event = Event(id=eventID)
        del eventID

        await self.config.authProvider.authorizeRequest(
            request, event, Authorization.readIncidents
        )

        try:
            incidentNumber = int(number)
        except ValueError:
            return notFoundResponse(request)
        del number

        try:
            incident = await self.config.store.incidentWithNumber(
                event, incidentNumber
            )
        except NoSuchIncidentError:
            return notFoundResponse(request)

        data = jsonTextFromObject(jsonObjectFromModelObject(incident)).encode(
            "utf-8"
        )

        return jsonBytes(request, data)
Example #6
0
    async def readIncidentReportResource(
        self, request: IRequest, eventID: str, number: str
    ) -> KleinRenderable:
        """
        Incident report endpoint.
        """
        try:
            incidentReportNumber = int(number)
        except ValueError:
            self.config.authProvider.authenticateRequest(request)
            return notFoundResponse(request)
        del number

        event = Event(id=eventID)
        del eventID

        incidentReport = await self.config.store.incidentReportWithNumber(
            event, incidentReportNumber
        )

        await self.config.authProvider.authorizeRequestForIncidentReport(
            request, incidentReport
        )

        text = jsonTextFromObject(jsonObjectFromModelObject(incidentReport))

        return jsonBytes(request, text.encode("utf-8"))
    def test_events(self, broken: bool = False) -> None:
        """
        :meth:`DataStore.events` returns all events.
        """
        store = self.store()
        store._db.executescript(
            dedent("""
                insert into EVENT (NAME) values ('Event A');
                insert into EVENT (NAME) values ('Event B');
                """))

        if broken:
            store.bringThePain()

        events = frozenset(self.successResultOf(store.events()))

        self.assertEqual(events, {Event(id="Event A"), Event(id="Event B")})
Example #8
0
    async def test_concentricStreets(self) -> None:
        """
        :meth:`IMSDataStore.createConcentricStreet` returns the concentric
        streets for the given event.
        """
        for event, streetID, streetName in (
            (Event("Foo"), "A", "Alpha"),
            (Event("Foo Bar"), "B", "Bravo"),
            (Event("XYZZY"), "C", "Charlie"),
        ):
            store = await self.store()

            await store.createEvent(event)
            await store.storeConcentricStreet(event, streetID, streetName)

            concentricStreets = await store.concentricStreets(event)

            self.assertEqual(len(concentricStreets), 1)
            self.assertEqual(concentricStreets.get(streetID), streetName)
    def test_createEvent_error(self) -> None:
        """
        :meth:`DataStore.createEvent` raises `StorageError` if SQLite raises.
        """
        store = self.store()
        store.bringThePain()

        f = self.failureResultOf(store.createEvent(Event(id="x")))
        f.printTraceback()
        self.assertEqual(f.type, StorageError)
 def test_createEvent_duplicate(self) -> None:
     """
     :meth:`DataStore.createEvent` raises :exc:`StorageError` when given an
     event that already exists in the data store.
     """
     event = Event(id="foo")
     store = self.store()
     self.successResultOf(store.createEvent(event))
     f = self.failureResultOf(store.createEvent(event))
     self.assertEqual(f.type, StorageError)
Example #11
0
    async def listIncidentReportsResource(
        self, request: IRequest, eventID: str
    ) -> KleinRenderable:
        """
        Incident reports endpoint.
        """
        event = Event(id=eventID)
        del eventID

        try:
            await self.config.authProvider.authorizeRequest(
                request, event, Authorization.readIncidents
            )
            limitedAccess = False
        except NotAuthorizedError:
            await self.config.authProvider.authorizeRequest(
                request, event, Authorization.writeIncidentReports
            )
            limitedAccess = True

        incidentNumberText = queryValue(request, "incident")

        store = self.config.store

        incidentReports: Iterable[IncidentReport]
        if limitedAccess:
            incidentReports = (
                incidentReport
                for incidentReport in await store.incidentReports(event=event)
                if request.user.rangerHandle
                in (entry.author for entry in incidentReport.reportEntries)
            )
        elif incidentNumberText is None:
            incidentReports = await store.incidentReports(event=event)
        else:
            try:
                incidentNumber = int(incidentNumberText)
            except ValueError:
                return invalidQueryResponse(
                    request, "incident", incidentNumberText
                )

            incidentReports = await store.incidentReportsAttachedToIncident(
                event=event, incidentNumber=incidentNumber
            )

        stream = buildJSONArray(
            jsonTextFromObject(
                jsonObjectFromModelObject(incidentReport)
            ).encode("utf-8")
            for incidentReport in incidentReports
        )

        writeJSONStream(request, stream, None)
        return None
 def test_setWriters_error(self) -> None:
     """
     :meth:`DataStore.setWriters` raises :exc:`StorageError` when SQLite
     raises an exception.
     """
     event = Event(id="foo")
     store = self.store()
     self.successResultOf(store.createEvent(event))
     store.bringThePain()
     f = self.failureResultOf(store.setWriters(event, ()))
     self.assertEqual(f.type, StorageError)
Example #13
0
    async def test_createEvent(self) -> None:
        """
        :meth:`IMSDataStore.createEvent` creates the given event.
        """
        for eventName in ("Foo", "Foo Bar"):
            event = Event(id=eventName)

            store = await self.store()
            await store.createEvent(event)
            stored = frozenset(await store.events())
            self.assertEqual(stored, frozenset((event, )))
Example #14
0
    async def test_setWriters(self) -> None:
        """
        :meth:`IMSDataStore.setWriters` sets the write ACL for an event.
        """
        event = Event(id="Foo")

        for writers in ({"a"}, {"a", "b", "c"}):
            store = await self.store()
            await store.createEvent(event)
            await store.setWriters(event, writers)
            result = frozenset(await store.writers(event))
            self.assertEqual(result, writers)
Example #15
0
 async def viewDispatchQueuePage(self, request: IRequest,
                                 eventID: str) -> KleinRenderable:
     """
     Endpoint for the dispatch queue page.
     """
     event = Event(id=eventID)
     # FIXME: Not strictly required because the underlying data is
     # protected.
     # But the error you get is stupid, so let's avoid that for now.
     await self.config.authProvider.authorizeRequest(
         request, event, Authorization.readIncidents)
     return DispatchQueuePage(self.config, event)
Example #16
0
    async def locationsResource(self, request: IRequest,
                                eventID: str) -> KleinRenderable:
        """
        Location list endpoint.
        """
        event = Event(id=eventID)

        await self.config.authProvider.authorizeRequest(
            request, event, Authorization.readIncidents)

        data = self.config.locationsJSONBytes
        return jsonBytes(request, data, str(hash(data)))
Example #17
0
    async def test_createConcentricStreet(self) -> None:
        """
        :meth:`IMSDataStore.createConcentricStreet` creates a concentric
        streets for the given event.
        """
        for event, streetID, streetName in (
            (Event(id="Foo"), "A", "Alpha"),
            (Event(id="Foo Bar"), "B", "Bravo"),
            (Event(id="XYZZY"), "C", "Charlie"),
        ):
            store = await self.store()

            await store.createEvent(event)
            await store.createConcentricStreet(event=event,
                                               id=streetID,
                                               name=streetName)

            stored = await store.concentricStreets(event=event)

            self.assertEqual(len(stored), 1)
            self.assertEqual(stored.get(streetID), streetName)
Example #18
0
    async def test_createEvent_error(self) -> None:
        """
        :meth:`IMSDataStore.createEvent` raises `StorageError` if the store
        raises.
        """
        store = await self.store()
        store.bringThePain()

        try:
            await store.createEvent(Event(id="x"))
        except StorageError:
            pass
        else:
            self.fail("StorageError not raised")
Example #19
0
 async def viewIncidentReportsPage(self, request: IRequest,
                                   eventID: str) -> KleinRenderable:
     """
     Endpoint for the incident reports page.
     """
     event = Event(id=eventID)
     del eventID
     try:
         await self.config.authProvider.authorizeRequest(
             request, event, Authorization.readIncidents)
     except NotAuthorizedError:
         await self.config.authProvider.authorizeRequest(
             request, event, Authorization.writeIncidentReports)
     return IncidentReportsPage(config=self.config, event=event)
Example #20
0
    async def test_createEvent_duplicate(self) -> None:
        """
        :meth:`IMSDataStore.createEvent` raises :exc:`StorageError` when given
        an event that already exists in the data store.
        """
        event = Event(id="foo")
        store = await self.store()
        await store.createEvent(event)

        try:
            await store.createEvent(event)
        except StorageError:
            pass
        else:
            self.fail("StorageError not raised")
Example #21
0
    async def listIncidentsResource(self, request: IRequest,
                                    eventID: str) -> None:
        """
        Incident list endpoint.
        """
        event = Event(id=eventID)

        await self.config.authProvider.authorizeRequest(
            request, event, Authorization.readIncidents)

        stream = buildJSONArray(
            jsonTextFromObject(jsonObjectFromModelObject(incident)).encode(
                "utf-8")
            for incident in await self.config.store.incidents(event))

        writeJSONStream(request, stream, None)
Example #22
0
    async def test_setWriters_error(self) -> None:
        """
        :meth:`IMSDataStore.setWriters` raises :exc:`StorageError` when the
        store raises an exception.
        """
        event = Event(id="foo")
        store = await self.store()
        await store.createEvent(event)
        store.bringThePain()

        try:
            await store.setWriters(event, ())
        except StorageError:
            pass
        else:
            self.fail("StorageError not raised")
Example #23
0
    async def listIncidentReportsResource(
            self, request: IRequest) -> KleinRenderable:
        """
        Incident reports endpoint.
        """
        store = self.config.store

        eventID = queryValue(request, "event")
        incidentNumberText = queryValue(request, "incident")

        if eventID is None:
            return invalidQueryResponse(request, "event")

        if incidentNumberText is None:
            return invalidQueryResponse(request, "incident")

        if eventID == incidentNumberText == "":
            await self.config.authProvider.authorizeRequest(
                request, None, Authorization.readIncidentReports)
            incidentReports = await store.detachedIncidentReports()

        else:
            try:
                event = Event(id=eventID)
            except ValueError:
                return invalidQueryResponse(request, "event", eventID)

            try:
                incidentNumber = int(incidentNumberText)
            except ValueError:
                return invalidQueryResponse(request, "incident",
                                            incidentNumberText)

            await self.config.authProvider.authorizeRequest(
                request, event, Authorization.readIncidents)
            incidentReports = await store.incidentReportsAttachedToIncident(
                event=event, incidentNumber=incidentNumber)

        stream = buildJSONArray(
            jsonTextFromObject(jsonObjectFromModelObject(
                incidentReport)).encode("utf-8")
            for incidentReport in incidentReports)

        writeJSONStream(request, stream, None)
        return None
Example #24
0
    async def viewIncidentsResource(self, request: IRequest,
                                    eventID: str) -> KleinRenderable:
        """
        Event root page.

        This redirects to the event's incidents page.
        """
        event = Event(id=eventID)
        del eventID
        try:
            await self.config.authProvider.authorizeRequest(
                request, event, Authorization.readIncidents)
            url = URLs.viewIncidentsRelative
        except NotAuthorizedError:
            await self.config.authProvider.authorizeRequest(
                request, event, Authorization.writeIncidentReports)
            url = URLs.viewIncidentReportsRelative

        return redirect(request, url)
Example #25
0
    async def viewIncidentPage(self, request: IRequest, eventID: str,
                               number: str) -> KleinRenderable:
        """
        Endpoint for the incident page.
        """
        event = Event(id=eventID)

        numberValue: Optional[int]
        if number == "new":
            authz = Authorization.writeIncidents
            numberValue = None
        else:
            authz = Authorization.readIncidents
            try:
                numberValue = int(number)
            except ValueError:
                return notFoundResponse(request)

        await self.config.authProvider.authorizeRequest(request, event, authz)

        return IncidentPage(self.config, event, numberValue)
Example #26
0
    async def viewIncidentReportPage(self, request: IRequest, eventID: str,
                                     number: str) -> KleinRenderable:
        """
        Endpoint for the incident report page.
        """
        event = Event(id=eventID)
        del eventID

        incidentReportNumber: Optional[int]
        config = self.config
        if number == "new":
            await config.authProvider.authorizeRequest(
                request, event, Authorization.writeIncidentReports)
            incidentReportNumber = None
            del number
        else:
            try:
                incidentReportNumber = int(number)
            except ValueError:
                return notFoundResponse(request)
            del number

            try:
                incidentReport = await config.store.incidentReportWithNumber(
                    event, incidentReportNumber)
            except NoSuchIncidentReportError:
                await config.authProvider.authorizeRequest(
                    request, event, Authorization.readIncidents)
                return notFoundResponse(request)

            await config.authProvider.authorizeRequestForIncidentReport(
                request, incidentReport)

        return IncidentReportPage(config=config,
                                  event=event,
                                  number=incidentReportNumber)
)

from .base import (
    DataStoreTests,
    dateTimesEqualish,
    normalizeAddress,
    reportEntriesEqualish,
    storeConcentricStreet,
)
from ..._exceptions import NoSuchIncidentError, StorageError

Dict, Event, Optional, Set  # silence linter

__all__ = ()

anEvent = Event(id="foo")

anIncident = Incident(
    event=anEvent,
    number=0,
    created=DateTime.now(TimeZone.utc),
    state=IncidentState.new,
    priority=IncidentPriority.normal,
    summary="A thing happened",
    location=Location(name="There", address=None),
    rangerHandles=(),
    incidentTypes=(),
    reportEntries=(),
)

aReportEntry = ReportEntry(
Example #28
0
from ims.model import (
    Event,
    Incident,
    IncidentPriority,
    IncidentState,
    Location,
    ReportEntry,
    RodGarettAddress,
)

from .base import DataStoreTests, TestDataStoreABC
from .._exceptions import NoSuchIncidentError, StorageError

__all__ = ()

anEvent = Event(id="foo")
anEvent2 = Event(id="bar")

# Note: we add a TimeDelta to the created attribute of objects so that they
# don't have timestamps that are within the time resolution of some back-end
# data stores.

aNewIncident = Incident(
    event=anEvent,
    number=0,
    created=DateTime.now(TimeZone.utc) + TimeDelta(seconds=1),
    state=IncidentState.new,
    priority=IncidentPriority.normal,
    summary="A thing happened",
    location=Location(name="There", address=None),
    rangerHandles=(),
Example #29
0
    async def newIncidentReportResource(
        self, request: IRequest, eventID: str
    ) -> KleinRenderable:
        """
        New incident report endpoint.
        """
        event = Event(id=eventID)
        del eventID

        await self.config.authProvider.authorizeRequest(
            request, event, Authorization.writeIncidentReports
        )

        try:
            json = objectFromJSONBytesIO(request.content)
        except JSONDecodeError as e:
            return invalidJSONResponse(request, e)

        if json.get(IncidentReportJSONKey.event.value, event.id) != event.id:
            return badRequestResponse(
                "Event ID mismatch: "
                f"{json[IncidentReportJSONKey.event.value]} != {event.id}"
            )
        if json.get(IncidentReportJSONKey.incidentNumber.value):
            return badRequestResponse(
                "New incident report may not be attached to an incident: "
                f"{json[IncidentReportJSONKey.incidentNumber.value]}"
            )

        author = request.user.shortNames[0]
        now = DateTime.now(TimeZone.utc)
        jsonNow = jsonObjectFromModelObject(now)

        # Set JSON event id
        # Set JSON incident report number to 0
        # Set JSON incident report created time to now

        for incidentReportKey in (
            IncidentReportJSONKey.number,
            IncidentReportJSONKey.created,
        ):
            if incidentReportKey.value in json:
                return badRequestResponse(
                    request,
                    f"New incident report may not specify "
                    f"{incidentReportKey.value}",
                )

        json[IncidentReportJSONKey.event.value] = event.id
        json[IncidentReportJSONKey.number.value] = 0
        json[IncidentReportJSONKey.created.value] = jsonNow

        # If not provided, set JSON report entries to an empty list

        if IncidentReportJSONKey.reportEntries.value not in json:
            json[IncidentReportJSONKey.reportEntries.value] = []

        # Set JSON report entry created time to now
        # Set JSON report entry author
        # Set JSON report entry automatic=False

        for entryJSON in json[IncidentReportJSONKey.reportEntries.value]:
            for reportEntryKey in (
                ReportEntryJSONKey.created,
                ReportEntryJSONKey.author,
                ReportEntryJSONKey.automatic,
            ):
                if reportEntryKey.value in entryJSON:
                    return badRequestResponse(
                        request,
                        f"New report entry may not specify "
                        f"{reportEntryKey.value}",
                    )

            entryJSON[ReportEntryJSONKey.created.value] = jsonNow
            entryJSON[ReportEntryJSONKey.author.value] = author
            entryJSON[ReportEntryJSONKey.automatic.value] = False

        # Deserialize JSON incident report

        try:
            incidentReport = modelObjectFromJSONObject(json, IncidentReport)
        except JSONCodecError as e:
            return badRequestResponse(request, str(e))

        # Store the incident report

        incidentReport = await self.config.store.createIncidentReport(
            incidentReport, author
        )

        self._log.info(
            "User {author} created new incident report "
            "#{incidentReport.number} via JSON",
            author=author,
            incidentReport=incidentReport,
        )
        self._log.debug(
            "New incident report: {json}",
            json=jsonObjectFromModelObject(incidentReport),
        )

        request.setHeader("Incident-Report-Number", str(incidentReport.number))
        request.setHeader(
            HeaderName.location.value,
            f"{URLs.incidentNumber.asText()}/{incidentReport.number}",
        )
        return noContentResponse(request)
Example #30
0
    async def editIncidentReportResource(
        self, request: IRequest, eventID: str, number: str
    ) -> KleinRenderable:
        """
        Incident report edit endpoint.
        """
        event = Event(id=eventID)
        del eventID

        await self.config.authProvider.authorizeRequest(
            request, event, Authorization.writeIncidentReports
        )

        author = request.user.shortNames[0]

        try:
            incidentReportNumber = int(number)
        except ValueError:
            return notFoundResponse(request)
        del number

        store = self.config.store

        #
        # Attach to incident if requested
        #
        action = queryValue(request, "action")

        if action is not None:
            incidentNumberText = queryValue(request, "incident")

            if incidentNumberText is None:
                return invalidQueryResponse(request, "incident")

            try:
                incidentNumber = int(incidentNumberText)
            except ValueError:
                return invalidQueryResponse(
                    request, "incident", incidentNumberText
                )

            if action == "attach":
                await store.attachIncidentReportToIncident(
                    incidentReportNumber, event, incidentNumber, author
                )
            elif action == "detach":
                await store.detachIncidentReportFromIncident(
                    incidentReportNumber, event, incidentNumber, author
                )
            else:
                return invalidQueryResponse(request, "action", action)

        #
        # Get the edits requested by the client
        #
        try:
            edits = objectFromJSONBytesIO(request.content)
        except JSONDecodeError as e:
            return invalidJSONResponse(request, e)

        if not isinstance(edits, dict):
            return badRequestResponse(
                request, "JSON incident report must be a dictionary"
            )

        if (
            edits.get(IncidentReportJSONKey.number.value, incidentReportNumber)
            != incidentReportNumber
        ):
            return badRequestResponse(
                request, "Incident report number may not be modified"
            )

        UNSET = object()

        created = edits.get(IncidentReportJSONKey.created.value, UNSET)
        if created is not UNSET:
            return badRequestResponse(
                request, "Incident report created time may not be modified"
            )

        async def applyEdit(
            json: Mapping[str, Any],
            key: Enum,
            setter: Callable[[Event, int, Any, str], Awaitable[None]],
            cast: Optional[Callable[[Any], Any]] = None,
        ) -> None:
            _cast: Callable[[Any], Any]
            if cast is None:

                def _cast(obj: Any) -> Any:
                    return obj

            else:
                _cast = cast
            value = json.get(key.value, UNSET)
            if value is not UNSET:
                await setter(event, incidentReportNumber, _cast(value), author)

        await applyEdit(
            edits,
            IncidentReportJSONKey.summary,
            store.setIncidentReport_summary,
        )

        jsonEntries = edits.get(
            IncidentReportJSONKey.reportEntries.value, UNSET
        )
        if jsonEntries is not UNSET:
            now = DateTime.now(TimeZone.utc)

            entries = (
                ReportEntry(
                    author=author,
                    text=jsonEntry[ReportEntryJSONKey.text.value],
                    created=now,
                    automatic=False,
                )
                for jsonEntry in jsonEntries
            )

            await store.addReportEntriesToIncidentReport(
                event, incidentReportNumber, entries, author
            )

        return noContentResponse(request)