Example #1
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 #2
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 #3
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 #4
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"])

        return noContentResponse(request)
Example #5
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 #6
0
    async def editIncidentTypesResource(
        self, request: IRequest
    ) -> KleinRenderable:
        """
        Incident types 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:
            return badRequestResponse(
                request, "root: expected a dictionary."
            )

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

        store = self.config.store

        if adds:
            if type(adds) is not list:
                return badRequestResponse(
                    request, "add: expected a list."
                )
            for incidentType in adds:
                await store.createIncidentType(incidentType)

        if show:
            if type(show) is not list:
                return badRequestResponse(
                    request, "show: expected a list."
                )
            await store.showIncidentTypes(show)

        if hide:
            if type(hide) is not list:
                return badRequestResponse(
                    request, "hide: expected a list."
                )
            await store.hideIncidentTypes(hide)

        return noContentResponse(request)
    def loadFromEventJSON(
        self, event: Event, path: Path, trialRun: bool = False
    ) -> None:
        """
        Load event data from a file containing JSON.
        """
        with path.open() as fileHandle:
            eventJSON = objectFromJSONBytesIO(fileHandle)

            self._log.info("Creating event: {event}", event=event)
            self.createEvent(event)

            # Load incidents
            for incidentJSON in eventJSON:
                try:
                    eventID = incidentJSON.get(IncidentJSONKey.event.value)
                    if eventID is None:
                        incidentJSON[IncidentJSONKey.event.value] = event.id
                    else:
                        if eventID != event.id:
                            raise ValueError(
                                f"Event ID {eventID} != {event.id}"
                            )

                    incident = modelObjectFromJSONObject(
                        incidentJSON, Incident
                    )
                except ValueError as e:
                    if trialRun:
                        number = incidentJSON.get(IncidentJSONKey.number.value)
                        self._log.critical(
                            "Unable to load incident #{number}: {error}",
                            number=number, error=e,
                        )
                    else:
                        raise

                for incidentType in incident.incidentTypes:
                    self.createIncidentType(incidentType, hidden=True)

                self._log.info(
                    "Creating incident in {event}: {incident}",
                    event=event, incident=incident
                )
                if not trialRun:
                    self.importIncident(incident)
Example #8
0
    def loadFromEventJSON(self,
                          event: Event,
                          path: Path,
                          trialRun: bool = False) -> None:
        """
        Load event data from a file containing JSON.
        """
        with path.open() as fileHandle:
            eventJSON = objectFromJSONBytesIO(fileHandle)

            self._log.info("Creating event: {event}", event=event)
            self.createEvent(event)

            # Load incidents
            for incidentJSON in eventJSON:
                try:
                    eventID = incidentJSON.get(IncidentJSONKey.event.value)
                    if eventID is None:
                        incidentJSON[IncidentJSONKey.event.value] = event.id
                    else:
                        if eventID != event.id:
                            raise ValueError(
                                f"Event ID {eventID} != {event.id}")

                    incident = modelObjectFromJSONObject(
                        incidentJSON, Incident)
                except ValueError as e:
                    if trialRun:
                        number = incidentJSON.get(IncidentJSONKey.number.value)
                        self._log.critical(
                            "Unable to load incident #{number}: {error}",
                            number=number,
                            error=e,
                        )
                    else:
                        raise

                for incidentType in incident.incidentTypes:
                    self.createIncidentType(incidentType, hidden=True)

                self._log.info("Creating incident in {event}: {incident}",
                               event=event,
                               incident=incident)
                if not trialRun:
                    self.importIncident(incident)
Example #9
0
    async def editIncidentTypesResource(
        self, request: IRequest
    ) -> KleinRenderable:
        """
        Incident types 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:
            return badRequestResponse(request, "root: expected a dictionary.")

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

        store = self.config.store

        if adds:
            if type(adds) is not list:
                return badRequestResponse(request, "add: expected a list.")
            for incidentType in adds:
                await store.createIncidentType(incidentType)

        if show:
            if type(show) is not list:
                return badRequestResponse(request, "show: expected a list.")
            await store.showIncidentTypes(show)

        if hide:
            if type(hide) is not list:
                return badRequestResponse(request, "hide: expected a list.")
            await store.hideIncidentTypes(hide)

        return noContentResponse(request)
Example #10
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 #11
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)
Example #12
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 #13
0
    async def editIncidentResource(
        self, request: IRequest, eventID: str, number: str
    ) -> KleinRenderable:
        """
        Incident edit endpoint.
        """
        event = Event(id=eventID)
        del eventID

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

        author = request.user.shortNames[0]

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

        #
        # 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 must be a dictionary"
            )

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

        UNSET = object()

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

        IncidentAttributeSetter = Callable[
            [Event, int, Any, str], Awaitable[None]
        ]

        async def applyEdit(
            json: Mapping[str, Any],
            key: Enum,
            setter: IncidentAttributeSetter,
            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, incidentNumber, _cast(value), author)

        store = self.config.store

        try:
            await applyEdit(
                edits,
                IncidentJSONKey.priority,
                store.setIncident_priority,
                lambda json: modelObjectFromJSONObject(json, IncidentPriority),
            )
            await applyEdit(
                edits,
                IncidentJSONKey.state,
                store.setIncident_state,
                lambda json: modelObjectFromJSONObject(json, IncidentState),
            )
        except JSONCodecError as e:
            return badRequestResponse(request, str(e))

        await applyEdit(
            edits, IncidentJSONKey.summary, store.setIncident_summary
        )
        await applyEdit(
            edits, IncidentJSONKey.rangerHandles, store.setIncident_rangers
        )
        await applyEdit(
            edits,
            IncidentJSONKey.incidentTypes,
            store.setIncident_incidentTypes,
        )

        location = edits.get(IncidentJSONKey.location.value, UNSET)
        if location is not UNSET:
            if location is None:
                for setter in (
                    store.setIncident_locationName,
                    store.setIncident_locationConcentricStreet,
                    store.setIncident_locationRadialHour,
                    store.setIncident_locationRadialMinute,
                    store.setIncident_locationDescription,
                ):
                    cast(IncidentAttributeSetter, setter)(
                        event, incidentNumber, None, author
                    )
            else:
                await applyEdit(
                    location,
                    LocationJSONKey.name,
                    store.setIncident_locationName,
                )
                await applyEdit(
                    location,
                    RodGarettAddressJSONKey.concentric,
                    store.setIncident_locationConcentricStreet,
                )
                await applyEdit(
                    location,
                    RodGarettAddressJSONKey.radialHour,
                    store.setIncident_locationRadialHour,
                )
                await applyEdit(
                    location,
                    RodGarettAddressJSONKey.radialMinute,
                    store.setIncident_locationRadialMinute,
                )
                await applyEdit(
                    location,
                    RodGarettAddressJSONKey.description,
                    store.setIncident_locationDescription,
                )

        jsonEntries = edits.get(IncidentJSONKey.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.addReportEntriesToIncident(
                event, incidentNumber, entries, author
            )

        return noContentResponse(request)
Example #14
0
    async def newIncidentResource(
        self, request: IRequest, eventID: str
    ) -> KleinRenderable:
        """
        New incident endpoint.
        """
        event = Event(id=eventID)
        del eventID

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

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

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

        # Set JSON incident number to 0
        # Set JSON incident created time to now

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

        json[IncidentJSONKey.number.value] = 0
        json[IncidentJSONKey.created.value] = jsonNow

        # If not provided, set JSON event, state to new, priority to normal

        if IncidentJSONKey.event.value not in json:
            json[IncidentJSONKey.event.value] = event.id

        if IncidentJSONKey.state.value not in json:
            json[IncidentJSONKey.state.value] = IncidentStateJSONValue.new.value

        if IncidentJSONKey.priority.value not in json:
            json[
                IncidentJSONKey.priority.value
            ] = IncidentPriorityJSONValue.normal.value

        # If not provided, set JSON handles, types, entries,
        # incident report numbers to an empty list

        for incidentKey in (
            IncidentJSONKey.rangerHandles,
            IncidentJSONKey.incidentTypes,
            IncidentJSONKey.reportEntries,
            IncidentJSONKey.incidentReportNumbers,
        ):
            if incidentKey.value not in json:
                json[incidentKey.value] = []

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

        for entryJSON in json[IncidentJSONKey.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

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

        # Validate data

        if incident.event != event:
            return badRequestResponse(
                request,
                f"Incident's event {incident.event} does not match event in "
                f"URL {event}",
            )

        # Store the incident

        incident = await self.config.store.createIncident(incident, author)

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

        request.setHeader("Incident-Number", str(incident.number))
        request.setHeader(
            HeaderName.location.value,
            f"{URLs.incidentNumber.asText()}/{incident.number}",
        )
        return noContentResponse(request)
Example #15
0
    async def newIncidentResource(
        self, request: IRequest, eventID: str
    ) -> KleinRenderable:
        """
        New incident endpoint.
        """
        event = Event(id=eventID)

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

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

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

        # Set JSON incident number to 0
        # Set JSON incident created time to now

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

        json[IncidentJSONKey.number.value] = 0
        json[IncidentJSONKey.created.value] = jsonNow

        # If not provided, set JSON event, state to new, priority to normal

        if IncidentJSONKey.event.value not in json:
            json[IncidentJSONKey.event.value] = event.id

        if IncidentJSONKey.state.value not in json:
            json[IncidentJSONKey.state.value] = (
                IncidentStateJSONValue.new.value
            )

        if IncidentJSONKey.priority.value not in json:
            json[IncidentJSONKey.priority.value] = (
                IncidentPriorityJSONValue.normal.value
            )

        # If not provided, set JSON handles, types, entries,
        # incident report numbers to an empty list

        for incidentKey in (
            IncidentJSONKey.rangerHandles,
            IncidentJSONKey.incidentTypes,
            IncidentJSONKey.reportEntries,
            IncidentJSONKey.incidentReportNumbers,
        ):
            if incidentKey.value not in json:
                json[incidentKey.value] = []

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

        for entryJSON in json[IncidentJSONKey.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

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

        # Validate data

        if incident.event != event:
            return badRequestResponse(
                request,
                f"Incident's event {incident.event} does not match event in "
                f"URL {event}"
            )

        # Store the incident

        incident = await self.config.store.createIncident(incident, author)

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

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

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

        author = request.user.shortNames[0]

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

        #
        # 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 must be a dictionary"
            )

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

        UNSET = object()

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

        IncidentAttributeSetter = (
            Callable[[Event, int, Any, str], Awaitable[None]]
        )

        async def applyEdit(
            json: Mapping[str, Any], key: Enum,
            setter: IncidentAttributeSetter,
            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, number, _cast(value), author)

        store = self.config.store

        try:
            await applyEdit(
                edits, IncidentJSONKey.priority, store.setIncident_priority,
                lambda json: modelObjectFromJSONObject(json, IncidentPriority),
            )
            await applyEdit(
                edits, IncidentJSONKey.state,
                store.setIncident_state,
                lambda json: modelObjectFromJSONObject(json, IncidentState),
            )
        except JSONCodecError as e:
            return badRequestResponse(request, str(e))

        await applyEdit(
            edits, IncidentJSONKey.summary, store.setIncident_summary
        )
        await applyEdit(
            edits, IncidentJSONKey.rangerHandles, store.setIncident_rangers
        )
        await applyEdit(
            edits, IncidentJSONKey.incidentTypes,
            store.setIncident_incidentTypes,
        )

        location = edits.get(IncidentJSONKey.location.value, UNSET)
        if location is not UNSET:
            if location is None:
                for setter in (
                    store.setIncident_locationName,
                    store.setIncident_locationConcentricStreet,
                    store.setIncident_locationRadialHour,
                    store.setIncident_locationRadialMinute,
                    store.setIncident_locationDescription,
                ):
                    cast(IncidentAttributeSetter, setter)(
                        event, number, None, author
                    )
            else:
                await applyEdit(
                    location, LocationJSONKey.name,
                    store.setIncident_locationName
                )
                await applyEdit(
                    location, RodGarettAddressJSONKey.concentric,
                    store.setIncident_locationConcentricStreet
                )
                await applyEdit(
                    location, RodGarettAddressJSONKey.radialHour,
                    store.setIncident_locationRadialHour
                )
                await applyEdit(
                    location, RodGarettAddressJSONKey.radialMinute,
                    store.setIncident_locationRadialMinute
                )
                await applyEdit(
                    location, RodGarettAddressJSONKey.description,
                    store.setIncident_locationDescription
                )

        jsonEntries = edits.get(IncidentJSONKey.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.addReportEntriesToIncident(
                event, number, entries, author
            )

        return noContentResponse(request)
Example #17
0
    async def newIncidentReportResource(
        self, request: IRequest
    ) -> KleinRenderable:
        """
        New incident report endpoint.
        """
        await self.config.authProvider.authorizeRequest(
            request, None, Authorization.writeIncidentReports
        )

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

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

        # 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.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", incidentReport.number)
        request.setHeader(
            HeaderName.location.value,
            f"{URLs.incidentNumber.asText()}/{incidentReport.number}"
        )
        return noContentResponse(request)
Example #18
0
    async def editIncidentReportResource(
        self, request: IRequest, number: int
    ) -> KleinRenderable:
        """
        Incident report edit endpoint.
        """
        await self.config.authProvider.authorizeRequest(
            request, None, Authorization.writeIncidentReports
        )

        author = request.user.shortNames[0]

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

        store = self.config.store

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

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

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

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

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

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

            if action == "attach":
                await store.attachIncidentReportToIncident(
                    number, event, incidentNumber
                )
            elif action == "detach":
                await store.detachIncidentReportFromIncident(
                    number, event, incidentNumber
                )
            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, number) != number:
            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: NamedConstant,
            setter: Callable[[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(number, _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(
                number, entries, author
            )

        return noContentResponse(request)
Example #19
0
    def load(self) -> None:
        """
        Load the configuration.
        """
        command = basename(argv[0])

        configParser = ConfigParser()

        def readConfig(path: Optional[Path]) -> None:
            if path is None:
                self._log.info("No configuration file specified.")
                return

            for _okFile in configParser.read(str(path)):
                self._log.info(
                    "Read configuration file: {path}", path=path
                )
                break
            else:
                self._log.error(
                    "Unable to read configuration file: {path}", path=path
                )

        def valueFromConfig(
            section: str, option: str, default: Optional[str]
        ) -> Optional[str]:
            try:
                value = configParser.get(section, option)
                if value:
                    return value
                else:
                    return default
            except (NoSectionError, NoOptionError):
                return default

        def pathFromConfig(
            section: str, option: str, root: Path, segments: Tuple[str]
        ) -> Path:
            if section is None:
                text = None
            else:
                text = valueFromConfig(section, option, None)

            if text is None:
                path = root
                for segment in segments:
                    path = path / segment

            elif text.startswith("/"):
                path = Path(text)

            else:
                path = root
                for segment in text.split(pathsep):
                    path = path / segment

            return path

        readConfig(self.ConfigFile)

        if self.ConfigFile is None:
            defaultRoot = Path(getcwd())
        else:
            defaultRoot = self.ConfigFile.parent.parent

        self.HostName = valueFromConfig("Core", "Host", "localhost")
        self._log.info("HostName: {hostName}", hostName=self.HostName)

        self.Port = int(cast(str, valueFromConfig("Core", "Port", "8080")))
        self._log.info("Port: {port}", port=self.Port)

        self.ServerRoot = pathFromConfig(
            "Core", "ServerRoot", defaultRoot, cast(Tuple[str], ())
        )
        self._log.info("Server root: {path}", path=self.ServerRoot)

        self.ConfigRoot = pathFromConfig(
            "Core", "ConfigRoot", self.ServerRoot, ("conf",)
        )
        self._log.info("Config root: {path}", path=self.ConfigRoot)

        self.DataRoot = pathFromConfig(
            "Core", "DataRoot", self.ServerRoot, ("data",)
        )
        self._log.info("Data root: {path}", path=self.DataRoot)

        self.DatabasePath = pathFromConfig(
            "Core", "Database", self.DataRoot, ("db.sqlite",)
        )
        self._log.info("Database: {path}", path=self.DatabasePath)

        self.CachedResourcesPath = pathFromConfig(
            "Core", "CachedResources", self.DataRoot, ("cache",)
        )
        self._log.info(
            "CachedResourcesPath: {path}", path=self.CachedResourcesPath
        )

        self.LogLevelName = valueFromConfig("Core", "LogLevel", "info")
        self._log.info("LogLevel: {logLevel}", logLevel=self.LogLevelName)

        self.LogFormat = valueFromConfig("Core", "LogFormat", "text")
        self._log.info("LogFormat: {logFormat}", logFormat=self.LogFormat)

        self.LogFilePath = pathFromConfig(
            "Core", "LogFile", self.DataRoot, (f"{command}.log",)
        )
        self._log.info("LogFile: {path}", path=self.LogFilePath)

        admins = cast(str, valueFromConfig("Core", "Admins", ""))
        self.IMSAdmins: FrozenSet[str] = frozenset(
            a.strip() for a in admins.split(",")
        )
        self._log.info("Admins: {admins}", admins=self.IMSAdmins)

        active = (
            cast(str, valueFromConfig("Core", "RequireActive", "true")).lower()
        )
        if active in ("false", "no", "0"):
            self.RequireActive = False
        else:
            self.RequireActive = True
        self._log.info(
            "RequireActive: {active}", active=self.RequireActive
        )

        self.DMSHost     = valueFromConfig("DMS", "Hostname", None)
        self.DMSDatabase = valueFromConfig("DMS", "Database", None)
        self.DMSUsername = valueFromConfig("DMS", "Username", None)
        self.DMSPassword = valueFromConfig("DMS", "Password", None)

        self._log.info(
            "Database: {user}@{host}/{db}",
            user=self.DMSUsername, host=self.DMSHost, db=self.DMSDatabase,
        )

        self.MasterKey = valueFromConfig("Core", "MasterKey", None)

        #
        # Persist some objects
        #

        self.dms = DutyManagementSystem(
            host=self.DMSHost,
            database=self.DMSDatabase,
            username=self.DMSUsername,
            password=self.DMSPassword,
        )

        self.store: IMSDataStore = DataStore(dbPath=self.DatabasePath)

        self.authProvider = AuthProvider(
            store=self.store,
            dms=self.dms,
            requireActive=self.RequireActive,
            adminUsers=self.IMSAdmins,
            masterKey=self.MasterKey,
        )

        locationsPath = self.DataRoot / "locations.json"

        if locationsPath.is_file():
            with locationsPath.open() as jsonStrem:
                json = objectFromJSONBytesIO(jsonStrem)
            self._log.info("{count} locations", count=len(json))
            self.locationsJSONBytes = jsonTextFromObject(json).encode("utf-8")
        else:
            self._log.info("No locations file: {path}", path=locationsPath)
            self.locationsJSONBytes = jsonTextFromObject([]).encode("utf-8")
Example #20
0
 def fromIO(cls, store: IMSDataStore, io: BinaryIO) -> "JSONImporter":
     cls._log.info("Reading from JSON I/O...")
     return cls.fromJSON(store, objectFromJSONBytesIO(io))