Compare commits

..
Author SHA1 Message Date
james.maesandClaude Opus 5.5 7f8e926f0c fix(internal): run system work as a QSystemUserSession
Db.asSystem built an anonymous QSession subclass with every permission
but no token. Token-based authentication modules (OAuth2, Auth0) accept a
QSystemUserSession without a token and reject any other tokenless session,
so on such hosts every system read and write failed in session validation
(JWTDecodeException: The token is null). ConfigSync never mirrored the
code-defined connections and buckets, and the operation log, object index,
capabilities and protection/header rule scoping silently did nothing.

Use core QSystemUserSession(instance), which carries the same permissions
and all-access keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:12:32 -05:00
james.maes 4741285e96 Merge pull request #15 from QRun-IO/feature/orb-0.6.9
ci: qqq-orb 0.6.9 (refresh SNAPSHOT dependencies)
2026-10-14 10:46:16 -05:00
james.maesandClaude Opus 5.5 4cf6ed9d83 ci: qqq-orb 0.6.9 (refresh SNAPSHOT dependencies)
Moves qqq-orb@0.6.8 to qqq-orb@0.6.9, which passes --update-snapshots so cached SNAPSHOT dependencies are re-checked on every build (QRun-IO/qqq-orb#22).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:07:30 -05:00
james.maes 21d5172fc4 Merge pull request #14 from QRun-IO/feature/guardrails-rc6
build: QQQ 4.1.0-RC.6 + guardrails (parent 2.1.0, enforced)
2026-10-14 07:27:51 -05:00
james.maesandClaude Opus 5.5 5871f32b77 build: QQQ 4.1.0-RC.6 and qbit-build-parent 2.1.0 with guardrails enforced
Move the core baseline to the released 4.1.0-RC.6 (architecture review
finding F19, guardrail G7). Adopt qbit-build-parent 2.1.0 with the
architecture guardrails enforced (G1, G3, G4, G7) and tests in strict
raw-route mode (G2). Everything passes with no source changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:18:42 -05:00
james.maes 97a79e031d Merge pull request #13 from QRun-IO/feature/bucket-max-upload-bytes
feat(api): StorageBucket.maxUploadBytes()
2026-10-14 07:09:09 -05:00
james.maesandClaude Opus 5.5 6ffcd32f99 feat(api): StorageBucket.maxUploadBytes()
Expose a bucket's upload limit in the api package (null means no limit),
so consumers such as qbit-collaboration need not read the internal
StorageBucketConfig through config(). Found by the qbit-guardrails G1-arch
check (qbit-bom#10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:23:03 -05:00
james.maes 64d23b03af Merge pull request #11 from QRun-IO/feature/arch-s3factory
refactor(connections): core S3ClientFactory on QQQ RC.5 (#10)
2026-10-13 19:46:14 -05:00
7 changed files with 98 additions and 25 deletions

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
version: 2.1
orbs:
qqq-orb: kingsrook/qqq-orb@0.6.8
qqq-orb: kingsrook/qqq-orb@0.6.9
commands:
configure-package-install:
+1
View File
@@ -5,6 +5,7 @@ All notable changes to this project are documented here. The format follows Keep
## [0.1.0] - Unreleased
### Added
- `StorageBucket.maxUploadBytes()` in the `api` package: the bucket's upload limit in bytes (null when none), so other qbits need not call `config()`, which returns an internal type.
- **The service (P1).** `ObjectStorage.bucket(alias)` and `ObjectStorage.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.
+4 -2
View File
@@ -18,6 +18,8 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<qbit.guardrails.enforce>true</qbit.guardrails.enforce>
<qbit.guardrails.strictUiApiRoutes>true</qbit.guardrails.strictUiApiRoutes>
<revision>0.1.0-SNAPSHOT</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
@@ -27,7 +29,7 @@
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.0.0</version>
<version>2.1.0</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
@@ -246,7 +248,7 @@
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<qqq.snapshot.version>4.1.0-RC.5</qqq.snapshot.version>
<qqq.snapshot.version>4.1.0-RC.6</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -128,6 +128,18 @@ public final class StorageBucket
/*******************************************************************************
** The largest object this bucket accepts on upload, in bytes, or null when
** the bucket sets no limit. Callers outside this qbit use this instead of
** config(), which returns an internal type.
*******************************************************************************/
public Long maxUploadBytes()
{
return (bucket.getMaxUploadBytes());
}
/*******************************************************************************
** The bucket configuration.
*******************************************************************************/
@@ -40,8 +40,8 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
/*******************************************************************************
@@ -131,30 +131,15 @@ public final class Db
/*******************************************************************************
** A session that holds every permission and every all-access security key of
** the given instance (computed per instance, never cached across instances).
** The core system-user session (every permission and every all-access
** security key of the given instance, read per instance). It must be a
** QSystemUserSession: token-based authentication modules (OAuth2, Auth0)
** accept that class without a token and reject any other tokenless session,
** which made every system read and write fail on such hosts.
*******************************************************************************/
private static QSession systemSession(QInstance instance)
{
QSession session = new QSession()
{
/*******************************************************************************
** The system user holds every permission.
*******************************************************************************/
@Override
public boolean hasPermission(String permissionName)
{
return (true);
}
};
for(QSecurityKeyType keyType : instance.getSecurityKeyTypes().values())
{
if(keyType.getAllAccessKeyName() != null)
{
session.withSecurityKeyValue(keyType.getAllAccessKeyName(), true);
}
}
return (session);
return (new QSystemUserSession(instance));
}
@@ -51,6 +51,7 @@ class StandaloneTest
}
ObjectStorage.bucket("files").delete(DeleteRequest.of(key));
assertThat(ObjectStorage.bucket("files").head(key)).isEmpty();
assertThat(ObjectStorage.bucket("files").maxUploadBytes()).isEqualTo(ObjectStorage.bucket("files").config().getMaxUploadBytes());
}
@@ -0,0 +1,72 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.objectstorage.internal;
import com.kingsrook.qbits.objectstorage.ObjectStorageQBitConfig;
import com.kingsrook.qbits.objectstorage.testsupport.StorageTestSupport;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** Db.asSystem runs work as the core system user. Token-based authentication
** modules (OAuth2, Auth0) accept a QSystemUserSession without a token and
** reject any other tokenless session, so the class matters, not just the
** permissions.
*******************************************************************************/
class DbSystemSessionTest
{
/*******************************************************************************
** Reset the runtime and context after each test.
*******************************************************************************/
@AfterEach
void tearDown()
{
StorageTestSupport.tearDown();
}
/*******************************************************************************
** The work sees a QSystemUserSession with every all-access key, and the
** caller's session is restored afterwards.
*******************************************************************************/
@Test
void testAsSystemUsesCoreSystemUserSession() throws Exception
{
ObjectStorageQBitConfig config = new ObjectStorageQBitConfig().withBackendName(StorageTestSupport.MEMORY).withBucketScoping(false);
StorageTestSupport.setUp(config, instance -> instance.addSecurityKeyType(new QSecurityKeyType().withName("tenant").withAllAccessKeyName("tenantAllAccess")));
QSession caller = QContext.getQSession();
QSession inside = Db.asSystem(QContext::getQSession);
assertThat(inside).isInstanceOf(QSystemUserSession.class);
assertThat(inside.hasPermission("anything")).isTrue();
assertThat(inside.hasSecurityKeyValue("tenantAllAccess", true)).isTrue();
assertThat(QContext.getQSession()).isSameAs(caller);
}
}