Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
242b2696fb
|
||
|
|
eb9d163840
|
No files matched your search
+68
-11
@@ -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\/.*/]
|
||||
@@ -1,7 +0,0 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
jobs:
|
||||
maven:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
|
||||
secrets: inherit
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user