wrolpi/wrolpi/errors.py
lrnselfreliance f27720683d
Read-only SQL query endpoint for advanced users (#820)
* Read-only SQL query endpoint for advanced users

POST /api/db/query runs one read-only statement; GET /api/db/schema describes
the tables.  Both are refused unless `allow_database_query: true` is set in
wrolpi.yaml (there is no UI for it).  The connection is opened with mode=ro and
an authorizer that allows only reads; queries are interrupted after 10 seconds,
rows are capped, and long values are truncated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Database queries cannot be enabled through the API, and use bounded memory

The config update API refuses to change `allow_database_query`; only an edit of
wrolpi.yaml on the WROLPi can enable queries.  Query connections refuse values
over 32 MB and rows over 100 columns, rows are read one at a time, and the
response stops at 5 million characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The database query setting can be changed like any other config

WROLPi is meant to be as secure as a bookshelf in your home; a LAN user who can
reach the API is trusted, so gating one key in the config API adds nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Database queries can search FTS5 tables on SQLite 3.46

FTS5 reads `PRAGMA data_version` through the authorizer when it opens a table
on SQLite 3.46 (Debian 13), so every MATCH and the schema endpoint failed with
"vtable constructor failed".  A denied VACUUM INTO also explains itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A config status test attaches the shared context it reads

It passed only when an earlier test in the same worker had attached it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:34:00 -06:00

243 lines
6.4 KiB
Python

from http import HTTPStatus
from sanic import SanicException
class APIError(SanicException):
code = 'APIError'
summary = 'No summary defined'
status_code = HTTPStatus.INTERNAL_SERVER_ERROR
class UnknownDirectory(APIError):
code = 'UNKNOWN_DIRECTORY'
summary = 'The directory does not exist.'
status_code = HTTPStatus.NOT_FOUND
class UnknownFile(APIError):
code = 'UNKNOWN_FILE'
summary = 'The file does not exist.'
status_code = HTTPStatus.NOT_FOUND
class SearchEmpty(APIError):
code = 'SEARCH_EMPTY'
summary = 'Search is empty, search_str must have content.'
status_code = HTTPStatus.BAD_REQUEST
class ValidationError(APIError):
code = 'VALIDATION_ERROR'
summary = 'Could not validate the contents of the request'
status_code = HTTPStatus.BAD_REQUEST
class InvalidOrderBy(ValidationError):
code = 'INVALID_ORDER_BY'
summary = 'Invalid order_by'
status_code = HTTPStatus.BAD_REQUEST
class WROLModeEnabled(APIError):
code = 'WROL_MODE_ENABLED'
summary = 'This method is disabled while WROL Mode is enabled.'
status_code = HTTPStatus.FORBIDDEN
class InvalidDownload(APIError):
code = 'INVALID_DOWNLOAD'
summary = 'The URL cannot be downloaded.'
status_code = HTTPStatus.BAD_REQUEST
class UnknownDownload(APIError):
code = 'UNKNOWN_DOWNLOAD'
summary = 'The Download cannot be found.'
status_code = HTTPStatus.NOT_FOUND
class UnrecoverableDownloadError(APIError):
code = 'UNRECOVERABLE_DOWNLOAD_ERROR'
summary = 'Download experienced an error which cannot be recovered. Download will be deleted.'
status_code = HTTPStatus.BAD_REQUEST
class BotBlockedDownloadError(UnrecoverableDownloadError):
code = 'BOT_BLOCKED_DOWNLOAD_ERROR'
summary = 'Download was blocked by bot detection or invalid cookies. All pending downloads will be stopped.'
status_code = HTTPStatus.BAD_REQUEST
class InvalidFile(APIError):
code = 'INVALID_FILE'
summary = 'File does not exist or is a directory'
status_code = HTTPStatus.BAD_REQUEST
class NativeOnly(APIError):
code = 'NATIVE_ONLY'
summary = 'This functionality is only supported outside a docker container'
status_code = HTTPStatus.BAD_REQUEST
class HotspotError(APIError):
code = 'HOTSPOT_ERROR'
summary = 'Updating/accessing Hotspot encountered an error'
status_code = HTTPStatus.INTERNAL_SERVER_ERROR
class LogLevelError(APIError):
code = 'LOG_LEVEL_ERROR'
summary = 'Log level is invalid'
status_code = HTTPStatus.BAD_REQUEST
class UnknownArchive(APIError):
code = 'UNKNOWN_ARCHIVE'
summary = 'Unable to find the archive'
status_code = HTTPStatus.NOT_FOUND
class UnknownTag(APIError):
code = 'UNKNOWN_TAG'
summary = 'Unable to find the tag'
status_code = HTTPStatus.NOT_FOUND
class UsedTag(APIError):
code = 'USED_TAG'
summary = 'Tag is being used'
status_code = HTTPStatus.BAD_REQUEST
class InvalidTag(APIError):
code = 'INVALID_TAG'
summary = 'Tag is not allowed'
status_code = HTTPStatus.BAD_REQUEST
class FileGroupIsTagged(APIError):
code = 'FILE_GROUP_IS_TAGGED'
summary = 'FileGroup is tagged'
status_code = HTTPStatus.CONFLICT
class FileGroupAlreadyTagged(APIError):
code = 'FILE_GROUP_ALREADY_TAGGED'
summary = 'FileGroup is already tagged with this Tag'
status_code = HTTPStatus.CONFLICT
class FileUploadFailed(APIError):
code = 'FILE_UPLOAD_FAILED'
summary = 'Failed to upload file'
status_code = HTTPStatus.BAD_REQUEST
class FileConflict(APIError):
code = 'FILE_CONFLICT'
summary = 'File/directory already exists'
status_code = HTTPStatus.CONFLICT
class HotspotPasswordTooShort(APIError):
code = 'HOTSPOT_PASSWORD_TOO_SHORT'
summary = 'Hotspot password must be at least 8 characters'
status_code = HTTPStatus.BAD_REQUEST
class ShutdownFailed(APIError):
code = 'SHUTDOWN_FAILED'
summary = 'Unable to shutdown the WROLPi'
status_code = HTTPStatus.INTERNAL_SERVER_ERROR
class InvalidConfig(APIError):
code = 'INVALID_CONFIG'
summary = 'Config is invalid'
status_code = HTTPStatus.BAD_REQUEST
class ConfigNotImported(APIError):
code = 'CONFIG_NOT_IMPORTED'
summary = 'WROLPi is still starting, try again in a moment'
status_code = HTTPStatus.SERVICE_UNAVAILABLE
class NoPrimaryFile(APIError):
code = 'NO_PRIMARY_FILE'
summary = 'Could not find a primary file in the provided group'
status_code = HTTPStatus.INTERNAL_SERVER_ERROR
class InvalidDatetime(APIError):
code = 'INVALID_DATETIME'
summary = 'Unable to parse a datetime string'
status_code = HTTPStatus.INTERNAL_SERVER_ERROR
class InvalidDirectory(APIError):
code = 'INVALID_DIRECTORY'
summary = 'Directory is not valid or impossible'
status_code = HTTPStatus.BAD_REQUEST
class FileWorkerConflict(APIError):
code = 'FILE_WORKER_CONFLICT'
summary = 'Not possible while FileWorker is busy'
status_code = HTTPStatus.CONFLICT
class IgnoredDirectoryError(APIError):
code = 'IGNORED_DIRECTORY_ERROR'
summary = 'Directory is being ignored'
status_code = HTTPStatus.BAD_REQUEST
class DownloadError(APIError):
code = 'DOWNLOAD_ERROR'
summary = 'Unable to complete download'
status_code = HTTPStatus.INTERNAL_SERVER_ERROR
class UnknownCollection(APIError):
code = 'UNKNOWN_COLLECTION'
summary = 'Unable to find the collection'
status_code = HTTPStatus.NOT_FOUND
class UnsupportedArchive(APIError):
code = 'UNSUPPORTED_ARCHIVE'
summary = 'The file is not a supported archive format.'
status_code = HTTPStatus.BAD_REQUEST
class InvalidArchiveMember(APIError):
code = 'INVALID_ARCHIVE_MEMBER'
summary = 'The requested member does not exist in the archive.'
status_code = HTTPStatus.BAD_REQUEST
class UnknownJob(APIError):
code = 'UNKNOWN_JOB'
summary = 'The Job cannot be found.'
status_code = HTTPStatus.NOT_FOUND
class InvalidJob(APIError):
code = 'INVALID_JOB'
summary = 'The Job request is invalid.'
status_code = HTTPStatus.BAD_REQUEST
class DatabaseQueryDisabled(APIError):
code = 'DATABASE_QUERY_DISABLED'
summary = 'Database queries are disabled. Set `allow_database_query: true` in wrolpi.yaml, then import it.'
status_code = HTTPStatus.FORBIDDEN
class InvalidDatabaseQuery(APIError):
code = 'INVALID_DATABASE_QUERY'
summary = 'The database query is invalid, not read-only, or took too long.'
status_code = HTTPStatus.BAD_REQUEST