build: QQQ 4.1.0-RC.6, core table feature permissions, guardrails (#20) #27
No files matched your search
+3
-2
@@ -5,8 +5,9 @@ All notable changes to this project are documented here. Format: [Keep a Changel
|
||||
## [Unreleased]
|
||||
|
||||
### Changed (core baseline, #20 / F19)
|
||||
- Build against the released QQQ BOM `4.1.0-RC.5` through the existing `qqq.snapshot.version` property; no core SNAPSHOT is used. No Java API compatibility fixes were needed.
|
||||
- Core actions now enforce permissions for non-HTTP callers. Authentik-specific guards remain for direct util calls and domain rules. See [UPGRADE.md](UPGRADE.md) for host impact and the remaining test stand-in.
|
||||
- Build against the released QQQ BOM `4.1.0-RC.6` through the existing `qqq.snapshot.version` property; no core SNAPSHOT is used. Adopt `qbit-build-parent:2.1.0` with `qbit.guardrails.enforce=true`.
|
||||
- Core actions now enforce permissions for non-HTTP callers. Authentik-specific guards remain for direct util calls and domain rules. See [UPGRADE.md](UPGRADE.md) for host impact and feature registration requirements.
|
||||
- Replace the expired feature-catalog stand-in with real `TableFeaturePermissionsMetaData` registrations and remove the test-only `PolicyCatalog` permission-source overload. Feature discovery uses the core catalog without static mocking.
|
||||
|
||||
### Added (automated sessions)
|
||||
- `AuthentikGroupPermissionMapper` resolves tokenless automated sessions by a unique active Authentik email match, applying direct and inherited group permissions, static fallbacks and the sign-in cut-over guard while preserving existing session permissions.
|
||||
|
||||
@@ -67,7 +67,7 @@ QQQ permissions, held as `<name>.hasAccess`, granted through the group attribute
|
||||
|
||||
Policies also grant table feature permissions registered through core `TableFeaturePermissionsMetaData`: `analyze` (qbit-data-analysis) and `comment`, `attach`, `tag`, `share`, `task`, `assign` (qbit-collaboration), or any future registered feature. For example, `{"Effect":"Allow","Action":["analyze","comment"],"Resource":["person"]}` grants `person.analyze` and `person.comment`. The resource is the table's effective permission base name. Features must be granted explicitly: policy action `"*"` still means only `hasAccess`, `read`, `insert`, `edit`, `delete`.
|
||||
|
||||
The editor discovers features and labels through `PermissionsHelper.getAllAvailablePermissions`; they appear when the host's core version exposes them. The pinned RC.5 release does not include `TableFeaturePermissionsMetaData`; its later core implementation is a follow-up for the next baseline upgrade. See [the policy format](docs/POLICY-FORMAT.md) for validation and host customizer usage.
|
||||
The editor discovers registered features and labels through core RC.6's `PermissionsHelper.getAllAvailablePermissions`. The build uses `qbit-build-parent:2.1.0` with `qbit.guardrails.enforce=true`. See [the policy format](docs/POLICY-FORMAT.md) for validation and host customizer usage.
|
||||
|
||||
### Authentik side
|
||||
|
||||
@@ -82,7 +82,7 @@ See also the QQQ [adoption checklist](https://github.com/QRun-IO/qqq/blob/develo
|
||||
| Item | What to do |
|
||||
|---|---|
|
||||
| Coordinates | `com.kingsrook.qbits:qbit-authentik`. No release is on Maven Central yet; CircleCI publishes snapshots from `develop`. |
|
||||
| QQQ version | QQQ 4.1.0-RC.5 (released BOM, no core SNAPSHOT). Java 21. |
|
||||
| QQQ version | QQQ 4.1.0-RC.6 (released BOM, no core SNAPSHOT). Java 21. |
|
||||
| Infrastructure | Authentik, contract-tested against 2026.8.3. The API must be reachable from the app at `baseUrl`. |
|
||||
| Tables | Create `authentik_admin_action` and `authentik_approval_request` on your local RDBMS backend (`docs/schema.sql`) before turning write features on. |
|
||||
| Registration | `new AuthentikQBitProducer().withQBitConfig(config).produce(qInstance)`. |
|
||||
|
||||
+12
-17
@@ -1,8 +1,8 @@
|
||||
# Upgrading qbit-authentik
|
||||
|
||||
## QQQ 4.1.0-RC.5 baseline (#20 / F19)
|
||||
## QQQ 4.1.0-RC.6 baseline (#20 / F19)
|
||||
|
||||
Hosts should align their QQQ dependencies with the released `qqq-bom-pom:4.1.0-RC.5`
|
||||
Hosts should align their QQQ dependencies with the released `qqq-bom-pom:4.1.0-RC.6`
|
||||
and use Java 21. The historical `qqq-snapshot` profile and `qqq.snapshot.version`
|
||||
property names remain, but the imported core version is a release, not a SNAPSHOT.
|
||||
Core actions now enforce permissions and field security locks for non-HTTP callers
|
||||
@@ -11,21 +11,16 @@ The qbit keeps its guards, scope checks and sensitive-field stripping because th
|
||||
also protect direct backend calls and implement Authentik-specific rules.
|
||||
No qbit configuration, stored policy format or schema changes are introduced.
|
||||
|
||||
Stand-in audit against core tag `v4.1.0-RC.5`:
|
||||
|
||||
- `PolicyFeaturePermissionsTest` supplies a synthetic available-permission catalog:
|
||||
`STAND-IN(core=QRun-IO/qqq#1136, removeBy=4.1.0-RC.6)`. RC.6 is the removal target,
|
||||
subject to verifying that release contains the metadata hook. Retain malformed
|
||||
and duplicate catalog test cases when adding real metadata registration coverage.
|
||||
- The requested stand-in/RC grep found only the old baseline references; the broader
|
||||
audit found the test catalog above and stale core-enforcement explanations.
|
||||
No production bridge can safely be removed by this baseline bump.
|
||||
- `TableFeaturePermissionsMetaData` (core commit `9a302136d`, #1136) and client
|
||||
channels (core commit `741bb4974`, PR #1141) exist after RC.5, but neither commit
|
||||
is an ancestor of the RC.5 tag and neither API is present in that release.
|
||||
Follow up on a released baseline containing them: verify the policy editor's
|
||||
catalog with real feature metadata and adopt client-channel attribution and
|
||||
identity/credential channel restrictions. These features are not added here.
|
||||
The build adopts `qbit-build-parent:2.1.0` with `qbit.guardrails.enforce=true`.
|
||||
The expired #1136 test stand-in is removed: tests now register features through
|
||||
`TableFeaturePermissionsMetaData` and discover them through the real
|
||||
`PermissionsHelper.getAllAvailablePermissions`, as production hosts do. The
|
||||
unused explicit permission-source overload in `PolicyCatalog` is removed.
|
||||
Duplicate shared-base entries, replacement registrations and malformed suffixes
|
||||
remain covered. Hosts should register feature suffixes, labels and table
|
||||
applicability through the core metadata API; no synthetic catalog is needed.
|
||||
Client-channel attribution and identity/credential channel restrictions remain
|
||||
separate follow-up work.
|
||||
|
||||
## Permissions renamed `identity.*` → `authentik.*` (unreleased → first release)
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -36,7 +38,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>
|
||||
@@ -245,7 +247,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>
|
||||
|
||||
@@ -139,17 +139,7 @@ public final class PolicyCatalog
|
||||
*******************************************************************************/
|
||||
public static PolicyCatalog of(QInstance qInstance)
|
||||
{
|
||||
return (of(qInstance, PermissionsHelper.getAllAvailablePermissions(qInstance)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build using an explicit permission source, including features registered
|
||||
** by core. This needs no dependency on the feature metadata hook itself.
|
||||
*******************************************************************************/
|
||||
public static PolicyCatalog of(QInstance qInstance, Collection<AvailablePermission> availablePermissions)
|
||||
{
|
||||
Collection<AvailablePermission> availablePermissions = PermissionsHelper.getAllAvailablePermissions(qInstance);
|
||||
Map<String, String> appOfObject = appsOfObjects(qInstance);
|
||||
Map<String, Map<String, ResourceDraft>> byApp = new LinkedHashMap<>();
|
||||
|
||||
|
||||
+32
-41
@@ -26,28 +26,25 @@ import com.kingsrook.qbits.authentik.client.AuthentikActionUtil;
|
||||
import com.kingsrook.qbits.authentik.permissions.AccessComparison;
|
||||
import com.kingsrook.qbits.authentik.permissions.EffectivePermissionResolver;
|
||||
import com.kingsrook.qbits.authentik.permissions.GroupGraph;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.AvailablePermission;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.TableFeaturePermission;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.TableFeaturePermissionsMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Table features use generic permission suffixes from core's existing catalog
|
||||
** API. The metadata hook is absent from the pinned core RC.5 release.
|
||||
** STAND-IN(core=QRun-IO/qqq#1136, removeBy=4.1.0-RC.6)
|
||||
** Replace the synthetic feature catalog with metadata registration when upgrading.
|
||||
** Table features are discovered through core's registered metadata and catalog.
|
||||
*******************************************************************************/
|
||||
class PolicyFeaturePermissionsTest
|
||||
{
|
||||
@@ -63,31 +60,27 @@ class PolicyFeaturePermissionsTest
|
||||
QInstance instance = PolicyCatalogTest.instance();
|
||||
instance.addTable(new QTableMetaData().withName("person_records").withLabel("People").withBackendName("memory")
|
||||
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS).withPermissionBaseName("person")));
|
||||
instance.addTable(new QTableMetaData().withName("person_extra").withLabel("Extra People").withBackendName("memory")
|
||||
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)));
|
||||
TableFeaturePermissionsMetaData features = TableFeaturePermissionsMetaData.ofOrWithNew(instance);
|
||||
features.withPermission(feature("comment", "Comment", "person_records", "legacy"))
|
||||
.withPermission(feature("analyze", "Old label", "person_records"))
|
||||
.withPermission(feature("analyze", "Analyze", "person_records", "gate", "open_table"))
|
||||
.withPermission(feature("tag", "Tag", "shared_one", "shared_two"))
|
||||
.withPermission(feature("share", "Share", "person_extra"))
|
||||
.withPermission(feature("", "Empty", "person_records"));
|
||||
return (instance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The shape returned by PermissionsHelper when core registers a feature.
|
||||
** Register a feature only for the named tables; core supplies permission names.
|
||||
*******************************************************************************/
|
||||
private static AvailablePermission feature(String name, String label)
|
||||
private static TableFeaturePermission feature(String suffix, String label, String... tables)
|
||||
{
|
||||
return (new AvailablePermission().withName(name).withObjectName("People").withObjectType("Table").withPermissionType(label));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Deliberately unsorted; duplicates and standard suffixes are not features.
|
||||
*******************************************************************************/
|
||||
private static List<AvailablePermission> available()
|
||||
{
|
||||
return (List.of(feature("person.comment", "Comment"), feature("person.analyze", "Analyze"), feature("person.analyze", "Analyze"),
|
||||
feature("person.read", "Read"), feature("person.insert", "Insert"), feature("person.edit", "Edit"), feature("person.delete", "Delete"),
|
||||
feature("person.hasAccess", "Access"), feature("person_extra.share", "Share"), feature("person.", "Empty"),
|
||||
feature("gate.analyze", "Analyze"), feature("legacy.comment", "Comment"), feature("legacy.write", "Write"),
|
||||
feature("shared_base.tag", "Tag"), feature("open_table.analyze", "Analyze")));
|
||||
return (new TableFeaturePermission().withSuffix(suffix).withLabel(label)
|
||||
.withAppliesTo((instance, table) -> List.of(tables).contains(table.getName())));
|
||||
}
|
||||
|
||||
|
||||
@@ -108,11 +101,13 @@ class PolicyFeaturePermissionsTest
|
||||
@Test
|
||||
void catalogListsFeaturesSeparately()
|
||||
{
|
||||
PolicyCatalog catalog = PolicyCatalog.of(instance(), available());
|
||||
QInstance instance = instance();
|
||||
assertThat(PermissionsHelper.getAllAvailablePermissions(instance)).filteredOn(permission -> permission.getName().equals("shared_base.tag")).hasSize(2);
|
||||
PolicyCatalog catalog = PolicyCatalog.of(instance);
|
||||
assertThat(catalog.get("person").actions()).containsExactly(PolicyAction.READ, PolicyAction.INSERT, PolicyAction.EDIT, PolicyAction.DELETE);
|
||||
assertThat(catalog.get("person").features()).containsExactly(new PolicyCatalog.Feature("analyze", "Analyze"), new PolicyCatalog.Feature("comment", "Comment"));
|
||||
assertThat(catalog.get("gate").features()).containsExactly(new PolicyCatalog.Feature("analyze", "Analyze"));
|
||||
assertThat(catalog.get("legacy").features()).containsExactly(new PolicyCatalog.Feature("comment", "Comment"), new PolicyCatalog.Feature("write", "Write"));
|
||||
assertThat(catalog.get("legacy").features()).containsExactly(new PolicyCatalog.Feature("comment", "Comment"), new PolicyCatalog.Feature("write", "WRITE"));
|
||||
assertThat(catalog.get("shared_base").features()).containsExactly(new PolicyCatalog.Feature("tag", "Tag"));
|
||||
assertThat(catalog.get("open_table")).isNull();
|
||||
assertThat(catalog.permissionNames()).contains("person.analyze", "person.comment", "gate.analyze", "legacy.comment", "legacy.write", "shared_base.tag")
|
||||
@@ -138,7 +133,7 @@ class PolicyFeaturePermissionsTest
|
||||
{
|
||||
QInstance instance = instance();
|
||||
instance.getTable("person_records").withoutCapabilities(Capability.TABLE_QUERY, Capability.TABLE_GET, Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
|
||||
PolicyCatalog.Resource resource = PolicyCatalog.of(instance, available()).get("person");
|
||||
PolicyCatalog.Resource resource = PolicyCatalog.of(instance).get("person");
|
||||
assertThat(resource.actions()).isEmpty();
|
||||
assertThat(resource.features()).hasSize(2);
|
||||
}
|
||||
@@ -151,7 +146,7 @@ class PolicyFeaturePermissionsTest
|
||||
@Test
|
||||
void parserValidatesFeaturesPerResource() throws Exception
|
||||
{
|
||||
PolicyCatalog catalog = PolicyCatalog.of(instance(), available());
|
||||
PolicyCatalog catalog = PolicyCatalog.of(instance());
|
||||
assertThat(PolicyParser.parseStrict(document(List.of("read", "analyze"), "person"), FORMAT, catalog).permissions())
|
||||
.containsExactly("person.analyze", "person.read");
|
||||
assertThat(PolicyParser.parse(document(List.of("analyze"), "person", "gate"), FORMAT, catalog)).containsExactly("gate.analyze", "person.analyze");
|
||||
@@ -173,7 +168,7 @@ class PolicyFeaturePermissionsTest
|
||||
@Test
|
||||
void wildcardDoesNotGrantFeatures()
|
||||
{
|
||||
PolicyCatalog catalog = PolicyCatalog.of(instance(), available());
|
||||
PolicyCatalog catalog = PolicyCatalog.of(instance());
|
||||
assertThat(PolicyParser.parse(document(List.of("*"), "person"), FORMAT, catalog))
|
||||
.containsExactlyInAnyOrder("person.hasAccess", "person.read", "person.insert", "person.edit", "person.delete");
|
||||
assertThat(PolicyParser.parse(document(List.of("*", "analyze"), "person"), FORMAT, catalog)).hasSize(6).contains("person.analyze").doesNotContain("person.comment");
|
||||
@@ -187,7 +182,7 @@ class PolicyFeaturePermissionsTest
|
||||
@Test
|
||||
void generatorRoundTripsFeatures() throws Exception
|
||||
{
|
||||
PolicyCatalog catalog = PolicyCatalog.of(instance(), available());
|
||||
PolicyCatalog catalog = PolicyCatalog.of(instance());
|
||||
List<String> permissions = List.of("person.comment", "person.read", "person.analyze", "gate.analyze");
|
||||
JSONObject generated = PolicyGenerator.generate(permissions, FORMAT, catalog);
|
||||
String compact = PolicyGenerator.toCompact(generated);
|
||||
@@ -208,22 +203,18 @@ class PolicyFeaturePermissionsTest
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The real mapper discovers simulated core entries and adds their exact names.
|
||||
** The real mapper discovers registered core features and adds their exact names.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void claimMapperUsesCoreCatalog()
|
||||
{
|
||||
QInstance instance = instance();
|
||||
try(MockedStatic<PermissionsHelper> helper = Mockito.mockStatic(PermissionsHelper.class, Mockito.CALLS_REAL_METHODS))
|
||||
{
|
||||
helper.when(() -> PermissionsHelper.getAllAvailablePermissions(instance)).thenReturn(available());
|
||||
QSession session = new QSession();
|
||||
session.setPermissions(Set.of("existing.read"));
|
||||
JSONObject claims = new JSONObject().put(FORMAT.claim(), document(List.of("analyze"), "person"));
|
||||
new AuthentikPolicyClaimMapper().customizeSession(instance, session, Map.of(PolicyPermissions.ID_TOKEN_CONTEXT_KEY, claims));
|
||||
assertThat(session.getPermissions()).containsExactlyInAnyOrder("existing.read", "person.analyze");
|
||||
assertThat(PolicyPermissions.fromClaims(claims, FORMAT, PolicyCatalog.of(instance))).containsExactly("person.analyze");
|
||||
}
|
||||
QSession session = new QSession();
|
||||
session.setPermissions(Set.of("existing.read"));
|
||||
JSONObject claims = new JSONObject().put(FORMAT.claim(), document(List.of("analyze"), "person"));
|
||||
new AuthentikPolicyClaimMapper().customizeSession(instance, session, Map.of(PolicyPermissions.ID_TOKEN_CONTEXT_KEY, claims));
|
||||
assertThat(session.getPermissions()).containsExactlyInAnyOrder("existing.read", "person.analyze");
|
||||
assertThat(PolicyPermissions.fromClaims(claims, FORMAT, PolicyCatalog.of(instance))).containsExactly("person.analyze");
|
||||
}
|
||||
|
||||
|
||||
@@ -234,7 +225,7 @@ class PolicyFeaturePermissionsTest
|
||||
@Test
|
||||
void comparisonIncludesInheritedFeatures() throws Exception
|
||||
{
|
||||
PolicyCatalog catalog = PolicyCatalog.of(instance(), available());
|
||||
PolicyCatalog catalog = PolicyCatalog.of(instance());
|
||||
JSONObject parent = new JSONObject().put("pk", "parent").put("name", "Analysts")
|
||||
.put("attributes", new JSONObject().put(FORMAT.attribute(), document(List.of("analyze"), "person")));
|
||||
JSONObject child = new JSONObject().put("pk", "child").put("name", "Staff").put("parents", new JSONArray(List.of("parent")))
|
||||
|
||||
Reference in new issue
Block a user