Compare commits

..
2 Commits
Author SHA1 Message Date
james.maesandClaude Opus 5.5 242b2696fb fix: child lists serialize, plural table labels, area homes without duplicate counts
- CmsRuntime no longer points back at its config; the config's runtime is
  @JsonIgnore. Tables carry their source qbit config to the frontend, and
  the cycle broke every child-record list (Sections, Items, Domains).
- Table labels are plural, as in kof22 (Pages, Page Section Types).
- Area homes show what needs attention (unpublished, inactive), the
  create actions the user may run, and recent changes. Next's app page
  already lists each table with its count, so the homes no longer repeat
  them.
- A test-scope demo server (CmsDemoServer) and screenshots of the homes
  and record screens at 390 and 1440 px, light and dark, from kof22's
  real CORE content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:45:43 -05:00
james.maesandClaude Opus 5.5 eb9d163840 feat: CMS qbit skeleton with feature flags and the CORE feature
Phase 1 of the qbit-cms spec (QRun-IO/qqq#1083), first slice.

- CmsQBitProducer builds everything from CmsQBitConfig (no package scan):
  validation, namespaces for a second install, and a collision check
  against names already on the instance.
- CmsFeature flags with dependencies; CORE implemented, the others
  rejected until they ship.
- CORE tables as standard QQQ tables (site, domain, redirect, redirect
  type, page, page type, page section, page section type, snippet, menu,
  menu item): prefixed names equal to the SQL names, snake_case columns,
  possible-value sources, child lists and associations, permission
  rules, field audit when the host has QQQ audit tables.
- The CMS app with one home per area (Sites, Pages, Navigation): real
  data read as the user (PermissionEnforcement.ENFORCE), honest empty
  states, readable at any width in light and dark mode.
- CmsRole bundles, CmsSecurity.applySessionPermissions, openReads.
- A prefix-parameterized Liquibase changelog (1.0.0 baseline, kof22
  column types, generic reference data, feature gating, halt on a
  foreign table with a CMS name) and generated reference DDL.
- Startup schema check (FAIL_FAST, DEGRADED, SKIP).
- Checkstyle wired into verify; 22 tests incl. Testcontainers PostgreSQL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:35:25 -05:00
10 changed files with 72 additions and 532 deletions

No files matched your search

+68 -11
View File
@@ -1,14 +1,71 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.8
qqq-orb: kingsrook/qqq-orb@0.6.8
commands:
configure-package-install:
steps:
- run:
name: Keep package installation unattended
command: |
# These build machines are discarded after the job.
sudo install -d /etc/needrestart/conf.d
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
workflows:
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: '0.8'
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
only: /^v\d+\.\d+\.\d+$/
test_only:
jobs:
- qqq-orb/mvn_test_only:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
filters:
branches:
ignore: /(develop|release\/.*|hotfix\/.*)/
publish_snapshot:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: snapshot
filters:
branches:
only: [develop]
publish_release_candidate:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release_candidate
filters:
branches:
only: [/release\/.*/]
publish_release:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: release
filters:
branches:
ignore: /.*/
tags:
only: [/^v[0-9]+\.[0-9]+\.[0-9]+$/]
publish_hotfix_release:
jobs:
- qqq-orb/mvn_publish:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
branch_type: hotfix
filters:
branches:
only: [/hotfix\/.*/]
-7
View File
@@ -1,7 +0,0 @@
name: build
on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
-23
View File
@@ -1,23 +0,0 @@
name: CodeQL
'on':
push:
branches:
- main
- develop
- release/**
- hotfix/**
pull_request: {}
schedule:
- cron: 41 16 * * 0
workflow_dispatch: {}
permissions:
contents: read
actions: read
security-events: write
packages: read
concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
analyze:
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
-74
View File
@@ -1,74 +0,0 @@
name: Release candidate
'on':
workflow_dispatch:
inputs:
dry_run:
description: Plan a cut without changing release state
type: boolean
default: false
finalize:
description: Retry finalization of an existing cut; do not cut another RC
type: boolean
default: false
recover:
description: Recover the same published RC after a deployment timeout; never
reupload
type: boolean
default: false
circle_build:
description: Original Circle publisher build number (recovery only)
type: string
default: ''
merge_sha:
description: Existing cut merge_sha (finalize retry only)
type: string
default: ''
bump_sha:
description: Existing cut bump_sha (finalize retry only)
type: string
default: ''
branch:
description: Existing cut branch (finalize retry only)
type: string
default: ''
version:
description: Existing cut version (finalize retry only)
type: string
default: ''
pull_request:
types:
- closed
branches:
- develop
repository_dispatch:
types:
- qbit-rc-published
permissions:
contents: write
actions: read
checks: read
jobs:
release-rc:
if: github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch'
|| (github.event_name == 'pull_request' && github.event.pull_request.merged
== true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name,
'release:rc'))
uses: QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml@cc30f599e63c8e41629c99d23395bac1adb8bc44
with:
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
}}
finalize: ${{ github.event_name == 'repository_dispatch' || (github.event_name
== 'workflow_dispatch' && inputs.finalize) }}
recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false
}}
circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build
|| '' }}
merge_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.merge_sha
|| inputs.merge_sha || '' }}
bump_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.bump_sha
|| inputs.bump_sha || '' }}
branch: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch
|| inputs.branch || '' }}
version: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version
|| inputs.version || '' }}
secrets: inherit
-16
View File
@@ -1,16 +0,0 @@
profile: qbit
orb_version: '0.8'
default_branch: develop
java_version: '21'
browsers: true
api_checks: false
docs_only: false
release_on_main: false
release_tag_policy: stable
test_branch_policy: non-release
publish:
snapshot: true
release_candidate: true
release: true
asciidoc: false
release_completion: true
+4 -6
View File
@@ -18,7 +18,6 @@
<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>
<qqq.version>4.1.0-RC.26</qqq.version> <!-- feeds qbit-build-parent's qqq-bom-pom import -->
<revision>0.1.0-SNAPSHOT</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
@@ -30,7 +29,7 @@
<parent>
<groupId>com.kingsrook</groupId>
<artifactId>qbit-build-parent</artifactId>
<version>2.2.1</version>
<version>2.0.0</version>
</parent>
<groupId>com.kingsrook.qbits</groupId>
@@ -110,7 +109,7 @@
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-frontend-next</artifactId>
<version>1.0.0-RC.38</version>
<version>1.0.0-RC.14</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -217,15 +216,14 @@
<profiles>
<!-- Built against qqq 4.1 (QBitMetaDataProducer). Until qbit-build-parent -->
<!-- imports the released 4.1 BOM, this profile is active by default and -->
<!-- imports qqq-bom-pom 4.1.0-RC.21 ahead of the parent's 4.0.0 BOM. -->
<!-- imports qqq-bom-pom 4.1.0-RC.4 ahead of the parent's 4.0.0 BOM. -->
<profile>
<id>qqq-snapshot</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<!-- The published QQQ 4.1 core (Central JAR/POM), the one BOM version shared by the four app-QBit patches. -->
<qqq.snapshot.version>4.1.0-RC.26</qqq.snapshot.version>
<qqq.snapshot.version>4.1.0-RC.4</qqq.snapshot.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -65,7 +65,6 @@ public class CmsQBitConfig implements QBitConfig
private String parentAppName;
private Boolean openReads = false;
private CmsStartupMode startupMode = CmsStartupMode.FAIL_FAST;
private CmsSchemaVerification schemaVerification = CmsSchemaVerification.CHANGELOG_HISTORY;
private Map<String, CmsRole> roleMapping = new LinkedHashMap<>();
private CmsRuntime runtime;
@@ -99,7 +98,6 @@ public class CmsQBitConfig implements QBitConfig
}
assertCondition(roleMapping != null, "roleMapping may be empty but not null", errors);
assertCondition(startupMode != null, "startupMode is required", errors);
assertCondition(schemaVerification != null, "schemaVerification is required", errors);
}
@@ -467,37 +465,6 @@ public class CmsQBitConfig implements QBitConfig
/*******************************************************************************
** Getter for schemaVerification
*******************************************************************************/
public CmsSchemaVerification getSchemaVerification()
{
return (this.schemaVerification);
}
/*******************************************************************************
** Setter for schemaVerification
*******************************************************************************/
public void setSchemaVerification(CmsSchemaVerification schemaVerification)
{
this.schemaVerification = schemaVerification;
}
/*******************************************************************************
** Fluent setter for schemaVerification
*******************************************************************************/
public CmsQBitConfig withSchemaVerification(CmsSchemaVerification schemaVerification)
{
this.schemaVerification = schemaVerification;
return (this);
}
/*******************************************************************************
** Getter for startupMode
*******************************************************************************/
@@ -29,8 +29,6 @@ import java.util.Locale;
import java.util.Set;
import com.kingsrook.qbits.cms.metadata.CmsTableDefinition;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaData;
@@ -70,10 +68,6 @@ public final class CmsSchemaCheck
List<String> problems = new ArrayList<>();
try(Connection connection = ConnectionManager.getConnection((RDBMSBackendMetaData) qInstance.getBackend(config.getBackendName())))
{
if(config.getSchemaVerification() == CmsSchemaVerification.CATALOG)
{
return (checkCatalog(qInstance, config, connection));
}
Set<String> recorded = recordedChangesets(connection);
if(recorded == null)
{
@@ -105,65 +99,6 @@ public final class CmsSchemaCheck
/*******************************************************************************
** Verify from the database catalog alone: each table of the installed features
** exists and has every column the qbit maps. Never reads Liquibase tables, so it
** works for an identity that has only table privileges.
*******************************************************************************/
static List<String> checkCatalog(QInstance qInstance, CmsQBitConfig config, Connection connection) throws SQLException
{
List<String> problems = new ArrayList<>();
for(CmsTableDefinition definition : CmsTables.definitions(config))
{
String table = config.applyPrefix(definition.logicalName());
if(!tableExists(connection, table))
{
problems.add("Table " + table + " is missing: run the qbit-cms changelog with cms.tablePrefix=" + config.getTableNamePrefix()
+ " and the " + definition.feature() + " feature in cms.features.");
continue;
}
QTableMetaData metaData = qInstance.getTable(table);
if(metaData == null)
{
continue;
}
Set<String> columns = columnsOf(connection, table);
for(QFieldMetaData field : metaData.getFields().values())
{
String column = field.getBackendName() != null ? field.getBackendName() : field.getName();
if(!columns.contains(column.toLowerCase(Locale.ROOT)))
{
problems.add("Column " + table + "." + column + " is missing: the database is behind this version of qbit-cms.");
}
}
}
return (problems);
}
/*******************************************************************************
** The lower-cased column names of a table, from the catalog.
*******************************************************************************/
static Set<String> columnsOf(Connection connection, String table) throws SQLException
{
Set<String> columns = new HashSet<>();
DatabaseMetaData metaData = connection.getMetaData();
for(String candidate : List.of(table, table.toUpperCase(Locale.ROOT), table.toLowerCase(Locale.ROOT)))
{
try(ResultSet resultSet = metaData.getColumns(connection.getCatalog(), null, candidate, null))
{
while(resultSet.next())
{
columns.add(resultSet.getString("COLUMN_NAME").toLowerCase(Locale.ROOT));
}
}
}
return (columns);
}
/*******************************************************************************
** The qbit's recorded changeset ids, or null when Liquibase has never run.
*******************************************************************************/
@@ -1,39 +0,0 @@
/*
* Copyright 2026 Kingsrook, LLC
*
* 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
*
* http://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.cms;
/*******************************************************************************
** What the startup schema check reads to decide the database is up to date.
*******************************************************************************/
public enum CmsSchemaVerification
{
/**
* The Liquibase history (DATABASECHANGELOG) records every changeset and the tables exist
* (the original check). Needs read access to the history table, which a DML-only
* runtime identity does not have.
*/
CHANGELOG_HISTORY,
/**
* Only the database catalog: every table of the installed features exists and has every
* column the qbit's metadata maps. Needs no Liquibase tables and no privilege beyond
* connecting, so it is the check for a runtime identity that owns nothing and may not
* read migration history.
*/
CATALOG
}
@@ -1,258 +0,0 @@
/*
* Copyright 2025 Kingsrook, LLC
*
* 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
*
* http://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.cms;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
import java.util.Set;
import com.kingsrook.qbits.cms.security.CmsRole;
import com.kingsrook.qbits.cms.security.CmsSecurity;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.module.postgres.model.metadata.PostgreSQLBackendMetaData;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/*******************************************************************************
** The install as production runs it on PostgreSQL 17: a schema OWNER runs the
** changelog (Liquibase 4.33.0) with a prefix that cannot collide with a host's own
** CMS tables, and the application runs as a DML-only role that owns nothing, cannot
** read migration history and cannot run DDL. The startup check must work for that
** role (catalog verification), and the QQQ permission ceilings must hold on top of
** the database grants.
*******************************************************************************/
@Testcontainers(disabledWithoutDocker = true)
class CmsPostgresRuntimeIdentityTest
{
private static final String OWNER = "qcms_owner";
private static final String RUNTIME = "qcms_runtime";
private static final String PREFIX = "qcms_";
@Container
private static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:17-alpine");
/*******************************************************************************
** Owner, runtime role, owner-run migration (twice), DML-only grants.
*******************************************************************************/
@BeforeAll
static void provision() throws Exception
{
try(Connection admin = connect(POSTGRES.getUsername(), POSTGRES.getPassword(), "postgres"); Statement statement = admin.createStatement())
{
statement.execute("CREATE ROLE " + OWNER + " LOGIN PASSWORD 'owner-synthetic'");
statement.execute("CREATE ROLE " + RUNTIME + " LOGIN PASSWORD 'runtime-synthetic'");
statement.execute("CREATE DATABASE cmsdb OWNER " + OWNER);
statement.execute("REVOKE ALL ON DATABASE cmsdb FROM PUBLIC");
statement.execute("GRANT CONNECT ON DATABASE cmsdb TO " + RUNTIME);
}
try(Connection owner = connect(OWNER, "owner-synthetic", "cmsdb"); Statement statement = owner.createStatement())
{
statement.execute("REVOKE ALL ON SCHEMA public FROM PUBLIC");
statement.execute("GRANT USAGE ON SCHEMA public TO " + RUNTIME);
}
ConnectionManager.resetConnectionProviders();
PostgreSQLBackendMetaData ownerBackend = backend(OWNER, "owner-synthetic");
CmsTestSupport.migrate(ownerBackend, Map.of("cms.tablePrefix", PREFIX));
CmsTestSupport.migrate(ownerBackend, Map.of("cms.tablePrefix", PREFIX));
try(Connection owner = connect(OWNER, "owner-synthetic", "cmsdb"); Statement statement = owner.createStatement())
{
statement.execute("DO $$ DECLARE t text; BEGIN FOR t IN SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE '" + PREFIX
+ "%' LOOP EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON %I TO " + RUNTIME + "', t); END LOOP; END $$");
}
}
/*******************************************************************************
** Clear the context.
*******************************************************************************/
@AfterEach
void afterEach()
{
QContext.clear();
}
/*******************************************************************************
** The original history check cannot run as the DML-only role and must fail
** closed; the catalog check starts it.
*******************************************************************************/
@Test
void testStartupCheckAsRuntimeRole() throws Exception
{
CmsQBitConfig history = config().withSchemaVerification(CmsSchemaVerification.CHANGELOG_HISTORY);
assertThatThrownBy(() -> new CmsQBitProducer().withQBitConfig(history).produce(runtimeInstance()))
.isInstanceOf(QException.class).hasMessageContaining("not up to date");
CmsQBitConfig catalog = config().withSchemaVerification(CmsSchemaVerification.CATALOG);
new CmsQBitProducer().withQBitConfig(catalog).produce(runtimeInstance());
assertThat(catalog.getRuntime().getSchemaProblems()).isEmpty();
}
/*******************************************************************************
** The catalog check names a missing table and a missing column, and refuses to
** start in FAIL_FAST mode.
*******************************************************************************/
@Test
void testCatalogCheckFindsDrift() throws Exception
{
CmsQBitConfig unmigrated = new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND).withTableNamePrefix("zcms_").withNamePrefix("zcms")
.withPermissionBaseName("zcms").withSchemaVerification(CmsSchemaVerification.CATALOG);
assertThatThrownBy(() -> new CmsQBitProducer().withQBitConfig(unmigrated).produce(runtimeInstance()))
.isInstanceOf(QException.class).hasMessageContaining("zcms_page is missing");
try(Connection owner = connect(OWNER, "owner-synthetic", "cmsdb"); Statement statement = owner.createStatement())
{
statement.execute("ALTER TABLE " + PREFIX + "page RENAME COLUMN title TO title_old");
try
{
CmsQBitConfig drifted = config().withSchemaVerification(CmsSchemaVerification.CATALOG);
assertThatThrownBy(() -> new CmsQBitProducer().withQBitConfig(drifted).produce(runtimeInstance()))
.isInstanceOf(QException.class).hasMessageContaining(PREFIX + "page.title is missing");
}
finally
{
statement.execute("ALTER TABLE " + PREFIX + "page RENAME COLUMN title_old TO title");
}
}
}
/*******************************************************************************
** The runtime role can do DML and nothing else: no DDL, no migration history.
*******************************************************************************/
@Test
void testRuntimeRoleIsDmlOnly() throws Exception
{
try(Connection runtime = connect(RUNTIME, "runtime-synthetic", "cmsdb"); Statement statement = runtime.createStatement())
{
assertThatThrownBy(() -> statement.execute("CREATE TABLE intruder (id int)")).isInstanceOf(SQLException.class).hasMessageContaining("permission denied");
assertThatThrownBy(() -> statement.execute("ALTER TABLE " + PREFIX + "site ADD COLUMN x int")).isInstanceOf(SQLException.class);
assertThatThrownBy(() -> statement.execute("DROP TABLE " + PREFIX + "site")).isInstanceOf(SQLException.class);
assertThatThrownBy(() -> statement.execute("TRUNCATE " + PREFIX + "site")).isInstanceOf(SQLException.class);
assertThatThrownBy(() -> statement.executeQuery("SELECT * FROM databasechangelog")).isInstanceOf(SQLException.class).hasMessageContaining("permission denied");
}
}
/*******************************************************************************
** Role ceilings over the DML grants, through QQQ's own actions on the native PG
** backend as the runtime role: an admin writes, a viewer reads only, an editor
** cannot change reference data, and a user with no CMS permission is refused.
*******************************************************************************/
@Test
void testRoleCeilingsAsRuntimeRole() throws Exception
{
CmsQBitConfig config = config().withSchemaVerification(CmsSchemaVerification.CATALOG);
QInstance instance = runtimeInstance();
instance.setEnforcePermissionsInCoreActions(true);
new CmsQBitProducer().withQBitConfig(config).produce(instance);
CmsTestSupport.session(instance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.ADMIN)));
Integer siteId = new InsertAction().execute(new InsertInput(PREFIX + "site").withRecord(new QRecord().withValue("name", "Example").withValue("shortName", "ex")))
.getRecords().get(0).getValueInteger("id");
assertThat(siteId).isNotNull();
CmsTestSupport.session(instance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.VIEWER)));
assertThat(new QueryAction().execute(new QueryInput(PREFIX + "site")).getRecords()).isNotEmpty();
assertThatThrownBy(() -> new InsertAction().execute(new InsertInput(PREFIX + "site").withRecord(new QRecord().withValue("name", "V").withValue("shortName", "v"))))
.isInstanceOf(QPermissionDeniedException.class);
CmsTestSupport.session(instance, CmsSecurity.permissionsFor(config, Set.of(CmsRole.EDITOR)));
assertThatThrownBy(() -> new InsertAction().execute(new InsertInput(PREFIX + "page_type").withRecord(new QRecord().withValue("name", "X"))))
.isInstanceOf(QPermissionDeniedException.class);
CmsTestSupport.session(instance, Set.of());
assertThatThrownBy(() -> new QueryAction().execute(new QueryInput(PREFIX + "site"))).isInstanceOf(QPermissionDeniedException.class);
assertThatThrownBy(() -> new QueryAction().execute(new QueryInput(PREFIX + "page"))).isInstanceOf(QPermissionDeniedException.class);
}
/*******************************************************************************
** The configuration under test: a prefix of its own, never the default.
*******************************************************************************/
private static CmsQBitConfig config()
{
return (new CmsQBitConfig().withBackendName(CmsTestSupport.BACKEND).withTableNamePrefix(PREFIX).withNamePrefix("qcms").withPermissionBaseName("qcms"));
}
/*******************************************************************************
** An application instance on the runtime role.
*******************************************************************************/
private static QInstance runtimeInstance()
{
ConnectionManager.resetConnectionProviders();
return (CmsTestSupport.blankInstance(backend(RUNTIME, "runtime-synthetic")));
}
/*******************************************************************************
** A native PostgreSQL backend as a named role.
*******************************************************************************/
private static PostgreSQLBackendMetaData backend(String user, String password)
{
return (new PostgreSQLBackendMetaData()
.withName(CmsTestSupport.BACKEND)
.withHostName(POSTGRES.getHost())
.withPort(POSTGRES.getFirstMappedPort())
.withDatabaseName("cmsdb")
.withUsername(user)
.withPassword(password));
}
/*******************************************************************************
** A JDBC connection as a named role.
*******************************************************************************/
private static Connection connect(String user, String password, String database) throws SQLException
{
return (DriverManager.getConnection("jdbc:postgresql://" + POSTGRES.getHost() + ":" + POSTGRES.getFirstMappedPort() + "/" + database, user, password));
}
}