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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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")
def fromIO(cls, store: IMSDataStore, io: BinaryIO) -> "JSONImporter": cls._log.info("Reading from JSON I/O...") return cls.fromJSON(store, objectFromJSONBytesIO(io))