diff --git a/CHANGELOG.md b/CHANGELOG.md index c8f169b..d1e405e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index a104a3d..2e8eb5f 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ QQQ permissions, held as `.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)`. | diff --git a/UPGRADE.md b/UPGRADE.md index 6f57785..5e8934c 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -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) diff --git a/pom.xml b/pom.xml index 48fe26a..a4320ba 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,8 @@ + true + true 0.1.0-SNAPSHOT 21 21 @@ -36,7 +38,7 @@ com.kingsrook qbit-build-parent - 2.0.0 + 2.1.0 com.kingsrook.qbits @@ -245,7 +247,7 @@ true - 4.1.0-RC.5 + 4.1.0-RC.6 diff --git a/src/main/java/com/kingsrook/qbits/authentik/policy/PolicyCatalog.java b/src/main/java/com/kingsrook/qbits/authentik/policy/PolicyCatalog.java index a330575..4f510d8 100644 --- a/src/main/java/com/kingsrook/qbits/authentik/policy/PolicyCatalog.java +++ b/src/main/java/com/kingsrook/qbits/authentik/policy/PolicyCatalog.java @@ -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 availablePermissions) - { + Collection availablePermissions = PermissionsHelper.getAllAvailablePermissions(qInstance); Map appOfObject = appsOfObjects(qInstance); Map> byApp = new LinkedHashMap<>(); diff --git a/src/test/java/com/kingsrook/qbits/authentik/policy/PolicyFeaturePermissionsTest.java b/src/test/java/com/kingsrook/qbits/authentik/policy/PolicyFeaturePermissionsTest.java index 321e308..0771495 100644 --- a/src/test/java/com/kingsrook/qbits/authentik/policy/PolicyFeaturePermissionsTest.java +++ b/src/test/java/com/kingsrook/qbits/authentik/policy/PolicyFeaturePermissionsTest.java @@ -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 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 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 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")))