mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
* 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>
243 lines
6.4 KiB
Python
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
|