Review of the first fix found the checker's pass too broad: it let any QSystemUserSession insert, update and delete every table nobody may write directly, so server code running as the system user could edit or delete operation log rows. - ObjectStoragePermissionChecker no longer looks at the session class. A write of those tables that reaches it is refused to every session, the core system user included. - Db.insert, Db.update and Db.delete mark their input with core's per-call permission bypass (withPermissionBypass, Db.OWN_WRITE_REASON), so core does not ask about the service's own writes when a host enforces permissions inside the core actions. - A failed operation log row is logged at ERROR, raises the storage.operation_log.failed alert on the StorageAlertSink (resolved by the next row written for the bucket) and is counted in OperationLog.failedWriteCount(). A row the insert action hands back with a record error is now detected too. The operation still succeeds. Tests: the service's own log, index insert, update and delete under core enforcement, with the acting person on the row; a system session outside Db refused insert, update and delete of an operation row; update and delete of an operation row refused to a person, the administrator and the system user; the failure signal. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
11 KiB
11 KiB
Changelog
All notable changes to this project are documented here. The format follows Keep a Changelog; versions follow semantic versioning.
[0.1.0] - Unreleased
Added
- Central health supplier
object_storage.connection.<id>(DEPENDENCY/STORAGE, 300s interval, 20000ms timeout), reusing bounded read-only capability probes with safe problem kinds and clock-skew details. Turn a connection's check off in the centralhealth_checktable. - In-memory health tests for registration, refresh, UP/DOWN/DEGRADED/UNKNOWN outcomes, deadlines, read-only execution and sentinel-secret exclusion.
- Expanded the host-neutral in-app help bundle to cover storage fields, actions, widgets and possible values, with live references and a
HelpBundleAssertionscoverage gate. Registered it for QQQ startup sync and HOST overrides; retained inline table/app help for hosts without help-content tables. - Every table explains itself: a help bundle (
help/qbit-object-storage.yaml, QQQ help-bundle format) says what eachobject_storage_*table is for and how its rows get created; it shows on the table screen and when the table is empty. Applied inline byObjectStorageHelpfor hosts without help-content tables; a host's own help is kept. - A Setup card on the Storage home (connections, buckets, header rules, protection rules) with the state and next step of each, resolved the way the runtime resolves them.
- A starter changelog
db/liquibase/qbit-object-storage/object-storage-defaults.changelog.sql: a disableddefault-mutableheader rule. Insert-only, run through real Liquibase inDefaultsChangelogTest. ObjectStorageTablesGuardrailTest(each table is used at runtime, seeded or explained, and has help).- The presign limits on the bucket row (
maxPresignGetSeconds,maxPresignPutSeconds) are now read at runtime (ConfigSync.effectiveMaxPresignGetSeconds/Put) as a lower cap: the effective limit is the smaller of the code config and the row. It applies to presigned GET, PUT, POST, previews and the Create download link action. Previously the row value was never read. object_storage_object_versionis documented as reserved and not used yet (the indexer lists current versions only).StorageBucket.maxUploadBytes()in theapipackage: the bucket's upload limit in bytes (null when none), so other qbits need not callconfig(), which returns an internal type.- The service (P1).
ObjectStorage.bucket(alias)andObjectStorage.asSystem(caller, runId): put (multipart above the part size), get with ranges, head, copy (in parts for very large objects), header and metadata changes, delete (delete marker, private trash, or permanent), restore, list, presigned GET, PUT and POST, public URLs, tags. Key building and URL encoding, the header policy (object_storage_header_rule),PublicUrlResolver,StorageChangeListener,StorageReferenceProvider,StorageAlertSink. - Configuration in code, upserted into visible
object_storage_*tables; secret references with an allow-list; per-connection rate limits; per-bucket and per-connection record security locks. - The capability model and the Test connection probe (read-only by default, write tests on request, clock skew, presign host), with seeded provider defaults.
- The
object_storage_operationaudit log (presigned URLs are logged without their signature) and protection rules (READ_ONLY,NO_DELETE,WARN, with allowed system callers). - Backups follow-ups SF1 to SF5:
BucketRole.BACKUP, object lock on put, system-caller permanent delete, probes that skip locked backup buckets, and the system-caller context. - The index (P2).
object_storage_objectandobject_storage_folderkept in step with each indexed bucket: resumable full scan with a mass-removal safety stop, HEAD backlog, write-through on every service write, soft-deleted rows, folder rollups. Orphaned and missing objects fromFieldReferenceProvider, daily usage snapshots, quota and freshness alerts, the nightly job. - The Storage app (P3, P4). Admin -> Storage with a responsive home; record screens with sections, child lists and a thumbnail preview; actions Rescan bucket, Check configuration, Refresh from bucket, Apply header policy (replaces app-specific backfills), Change headers and metadata, Edit tags, Copy, Move or rename, Delete, Restore, Create download link, Create upload link, folder rename and delete, Upload files, Test connection; every one a standard Review and Result with a bulk cap.
ObjectStorageSchema: PostgreSQL DDL generated from the table metadata.- Foreign keys on every relation between the qbit tables (
fk_<table>_<column>), generated byObjectStorageSchema: owned observed state (capabilities, folders, objects, versions, usage) cascades from its bucket or object,object.folder_idandobject_reference.object_idare set null, and a bucket's connection, header and protection rules and operation log restrict. Each key is added by an idempotent block that first records rows whose parent is gone inqqq_fk_quarantineand clears (nullable column) or deletes (required column) them, so it also upgrades an existing schema. New indexesix_object_storage_bucket_1(connection_id),ix_object_storage_header_rule_1andix_object_storage_protection_rule_1(bucket_id) back the keys. Upgrade: run the generated statements again (or the host's copy of them). - Conformance suite against MinIO and Garage (Testcontainers), service, index and action tests, and the index and actions on PostgreSQL.
Fixed
- The service's own writes are no longer refused in a host that enforces permissions inside the core actions (
QInstance.enforcePermissionsInCoreActions).Db.asSystemstarts a new context, so each of its writes is an outermost action there and core asksObjectStoragePermissionChecker, which refuses every write of a table nobody may write directly, to every session. Theobject_storage_operationrow of every stored object was lost (logged as "Could not write a storage operation log row", the object itself was stored), and so was each other system write of those tables.Db.insert,Db.updateandDb.deletenow mark their input with core's per-call permission bypass (withPermissionBypass, reasonDb.OWN_WRITE_REASON), so core does not ask about the service's own writes. The checker is unchanged in what it refuses: a write of those tables that does not carry a bypass is refused to a person, toobject_storage.adminand to the core system user alike, so server code running as the system user cannot edit or delete operation log rows through an enforced action. - An operation log row that could not be written is no longer only a warning. It is logged at ERROR, raises the
storage.operation_log.failedalert (StorageAlertSink, severityerror, dedupe keybucket:<alias>, resolved by the next row written for that bucket) and is counted inOperationLog.failedWriteCount()(per node, since start). A row the insert action hands back with a record error (a refused value, a record lock) is now detected as well; before, it was dropped without any log line from the qbit. The operation the row describes still succeeds.
Removed
- The QRun-IO/qqq#1165 stand-ins:
SessionCredentialsClient(session tokens now go through coreS3BackendMetaData.sessionToken/S3ClientFactory, including for the presigner) and the file reading inSecretRefs(coreQMetaDataVariableInterpreterresolves${file.PATH}). Behavior change: core reads${file.PATH}only inside its allowed roots, which are/var/run/secretsand/run/secretsby default. A host that mounts storage secrets elsewhere must setqqq.fileReference.rootsorQQQ_FILE_REFERENCE_ROOTS. The qbit's default allow-list (file./var/run/secrets/storage/*) is already inside the default roots. - None for the health migration; both Test connection record actions, including on-demand write tests, are retained.
Changed
- Multi-node deployments: the runtime caches are registered with QQQ
ClusterCaches. Shared (cleared on every node): the bucket rows' presign limits (qbit-object-storage.bucket-row-limits, on a bucket row insert or update), the enabled header rules (qbit-object-storage.header-rules, on a header rule insert, update or delete), both through the newConfigRowChangeCustomizer, and the capability statuses (qbit-object-storage.capabilities: a probe result recorded on one node clears that capability on the others, which reload it from the capability row; before, other nodes kept the old status until restart). The 15 s TTLs remain the safety net. Node-local, with reasons: the alias and connection row-id maps, the S3 client map, the process-permission map, the provider defaults, the running-scan guard and the per-pod presign rate limiter (StorageBucket.PRESIGN_COUNTS; per-node rate limits are accepted for now).ObjectStorageClusterCachesTestaudits the package for unregistered static caches and covers remote and local invalidation. - Builds against QQQ
4.1.0-SNAPSHOT(qqq.snapshot.version) forClusterCachesuntil the next QQQ RC ships; a release must pin that RC. - Read the qbit metadata version from the Maven-filtered artifact version so release metadata matches the build and help bundle.
- Rechecked the released RC.8 JARs: core still has no S3 session-token/provider hook or file-reference interpreter. Retained both compatibility shims and moved their review deadline to RC.9 under issue #10.
- Raised the QQQ BOM baseline to released
4.1.0-RC.8for the central health API. - Raised the QQQ BOM baseline to released
4.1.0-RC.7for the core help-bundle registry and coverage assertions. - Raised the QQQ BOM baseline to released
4.1.0-RC.5and delegated S3 client construction to coreS3ClientFactory.buildClient. Removed local HTTP client and trust-manager implementations; existing endpoint, region, addressing, checksum, TLS/CA, credentials and timeout settings remain supported. - Clients remain owned by each connection runtime and are rebuilt on reset/reinstall. Retry behavior now follows core/SDK defaults instead of forcing
ADAPTIVE_V2; hosts can select the SDK retry mode throughAWS_RETRY_MODEoraws.retryMode. - Retained local presigning (including a separate endpoint/key). A request-credentials bridge preserves static session tokens because RC.5 core metadata/factory has no session-token field or credentials-provider hook. This bridge and the existing
${file.PATH}resolver remain necessary in released RC.7 (no session-token/provider hook or file-reference interpreter); their review deadline is now RC.8 under issue #10. - Added the filesystem module with its unused SFTP/SSH stack excluded. Resolved dependencies otherwise only change QQQ RC.4 to RC.5 and jsoup 1.23.1 to 1.23.2; see dependency details.
Notes
- Deliberate differences from the design text: Test connection is two actions (read-only, and with write tests) so the write variant can require
object_storage.admin; a folder record has no subfolder child list (core does not allow a self join as an exposed join); there is nowithTableNamePrefix(theobject_storage_prefix is fixed by the naming rule).