Author SHA1 Message Date
dependabot[bot] 57b26d6107 chore(deps): Bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml](https://github.com/qrun-io/qqq-orb) and [QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml](https://github.com/qrun-io/qqq-orb).


Updates `QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml` from c288a8864fe9054f68410e1d78f5863aea4c4d22 to 1c389210e6dfda1630d053c07ae4dae523dd5576
- [Release notes](https://github.com/qrun-io/qqq-orb/releases)
- [Changelog](https://github.com/QRun-IO/qqq-orb/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/qrun-io/qqq-orb/compare/c288a8864fe9054f68410e1d78f5863aea4c4d22...1c389210e6dfda1630d053c07ae4dae523dd5576)

Updates `QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml` from cc30f599e63c8e41629c99d23395bac1adb8bc44 to 1c389210e6dfda1630d053c07ae4dae523dd5576
- [Release notes](https://github.com/qrun-io/qqq-orb/releases)
- [Changelog](https://github.com/QRun-IO/qqq-orb/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/qrun-io/qqq-orb/compare/cc30f599e63c8e41629c99d23395bac1adb8bc44...1c389210e6dfda1630d053c07ae4dae523dd5576)

---
updated-dependencies:
- dependency-name: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml
  dependency-version: 1c389210e6dfda1630d053c07ae4dae523dd5576
  dependency-type: direct:production
  dependency-group: github-actions
- dependency-name: QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml
  dependency-version: 1c389210e6dfda1630d053c07ae4dae523dd5576
  dependency-type: direct:production
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-28 08:14:05 +00:00
james.maesandCodex da332ce0fb ci: adopt managed orb 0.8 patch track (#22)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 15:28:56 -05:00
james.maesandCodex 22a40ad295 ci: adopt shared orb 0.7.18 (#21)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 14:46:50 -05:00
james.maesandCodex c92df228b9 ci: adopt shared orb 0.7.17 (#20)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 14:16:33 -05:00
james.maesandCodex 5af79ae05d ci: use the shared branch-aware CodeQL worker (#18)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 10:46:11 -05:00
james.maesandCodex 469e727884 ci: adopt central orb 0.7.12 and stable gates (#16)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 08:52:58 -05:00
james.maesandCodex 6434453666 ci: centralize rc release completion in qqq orb (#14)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 02:24:27 -05:00
james.maesandCodex f2584be8c5 ci: adopt shared orb 0.7.11 (#13)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 00:59:09 -05:00
james.maes 43d65e2e58 Merge pull request #12 from QRun-IO/feature/central-ci
ci: adopt shared QQQ orb standard
2026-10-20 19:20:54 -05:00
james.maesandCodex 6dbbdacd0e ci: adopt verified central QQQ orb 0.7.3
Co-Authored-By: Codex <noreply@openai.com>
2026-10-20 19:08:18 -05:00
james.maes 913bd497ed Merge pull request #11 from QRun-IO/feature/jobs-app-permission-gate
fix(security): gate the Jobs app and home widgets behind jobs.hasAccess
2026-10-16 10:19:12 -05:00
james.maesandClaude Opus 5.5 8cb487343a fix(security): gate the Jobs app and home widgets behind <appName>.hasAccess
The Jobs app and five of its six home widgets had no permission rules, so
the nav entry and home rendered for users with no permissions. They now
require <appName>.hasAccess (default jobs.hasAccess, exposed via
JobsQBitConfig.getHasAccessPermissionName()) with HIDDEN deny behaviour.
Workers keeps its jobs_admin rule. Record widgets, the jobs REST/UI API and
jobs_job record locks are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:06:34 -05:00
james.maes b8bc1a7719 Merge pull request #10 from QRun-IO/ci/release-rc-secrets-inherit
ci(release-rc): pass secrets so the shared workflow can sign the RC tag
2026-10-16 08:04:57 -05:00
james.maesandClaude Opus 5.5 dd06743824 ci(release-rc): pass secrets to the shared workflow so it can sign the RC tag
QRun-IO/.github#2 adds a tag-and-release job that signs vX with the
QRUN_TAG_SIGNING_KEY secret after Maven Central confirms the artifact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:21:14 -05:00
10 changed files with 261 additions and 73 deletions

No files matched your search

+11 -57
View File
@@ -1,60 +1,14 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.6.9
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
qqq-orb: kingsrook/qqq-orb@0.8
workflows:
test_only:
jobs:
- qqq-orb/mvn_test_only:
pre-steps:
- configure-package-install
context: [qqq-maven-registry-credentials]
filters:
branches:
ignore: /(develop|release\/.*)/
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\d+\.\d+\.\d+$/]
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: '0.8'
filters:
branches:
ignore: automation/qbit-rc-pending
tags:
only: /^v\d+\.\d+\.\d+$/
+23
View File
@@ -0,0 +1,23 @@
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@1c389210e6dfda1630d053c07ae4dae523dd5576
+62 -13
View File
@@ -1,25 +1,74 @@
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
# the release line and the release/* push runs CircleCI publish_release_candidate.
name: Release candidate
on:
'on':
workflow_dispatch:
inputs:
dry_run:
description: Print the plan without merging or creating branches
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]
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.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
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@1c389210e6dfda1630d053c07ae4dae523dd5576
with:
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
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
@@ -0,0 +1,16 @@
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
+6
View File
@@ -5,6 +5,12 @@ and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.0] - Unreleased
### Security
- The Jobs app and its five non-admin home widgets now require `<appName>.hasAccess`
(default `jobs.hasAccess`) and are hidden otherwise. Behaviour change: hosts must grant
this permission to the users who should see the Jobs app. Workers still needs `jobs_admin`.
### Changed
- Built on QQQ 4.1.0-RC.7; stand-ins removed.
+3
View File
@@ -42,6 +42,9 @@ The tables are core's, prefixed `jobs_`. This qbit adds no tables.
## Permissions
- Users see their own jobs: core's record lock on `jobs_job.userId` through the `jobs_user` key.
- The Jobs app and its home widgets require `<appName>.hasAccess` (default `jobs.hasAccess`, from
`JobsQBitConfig.getHasAccessPermissionName()`); without it they are hidden. Hosts must grant it to the
users who should see the Jobs app. The header Jobs tray and record locks are unaffected.
- `jobs_admin` sees and controls every job, and sees the Workers widget.
- Apply the keys in your auth customizer with `DurableJobsSecurity.applyToSession(session)`.
@@ -82,6 +82,15 @@ public class JobsQBitConfig implements QBitConfig
assertCondition(schedulerName == null || !schedulerName.isBlank(), "schedulerName must be null or nonblank", errors);
}
/*******************************************************************************
** The permission a user needs to see the Jobs app and its home widgets,
** "<appName>.hasAccess" (default "jobs.hasAccess"). Hosts must grant it.
*******************************************************************************/
public String getHasAccessPermissionName()
{
return (appName + ".hasAccess");
}
/*******************************************************************************
** Get appName.
*******************************************************************************/
@@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
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.processes.QBackendStepMetaData;
@@ -167,8 +168,16 @@ public class JobsQBitProducer implements QBitMetaDataProducer<JobsQBitConfig>
QWidgetMetaData workers = registerWidget(instance, "jobsHomeWorkers", "Workers", WidgetType.TABLE, qBitConfig.getHomeRefreshSeconds());
workers.withPermissionRules(adminRules());
registerWidget(instance, "jobsHomeHistory", "Job history", WidgetType.STACKED_BAR_CHART, qBitConfig.getHomeRefreshSeconds());
for(String name : HOME_WIDGETS)
{
if(!"jobsHomeWorkers".equals(name))
{
instance.getWidget(name).setPermissionRules(appAccessRules());
}
}
QAppMetaData app = new QAppMetaData().withName(qBitConfig.getAppName()).withLabel("Jobs").withIcon(new QIcon("pending_actions"))
.withChild(instance.getTable(JOBS_JOB)).withWidgets(new ArrayList<>(HOME_WIDGETS));
.withChild(instance.getTable(JOBS_JOB)).withWidgets(new ArrayList<>(HOME_WIDGETS))
.withPermissionRules(appAccessRules());
app.setParentAppName(qBitConfig.getParentAppName());
instance.addApp(app);
if(qBitConfig.getParentAppName() != null)
@@ -235,6 +244,16 @@ public class JobsQBitProducer implements QBitMetaDataProducer<JobsQBitConfig>
.withTier(Tier.T2).withWidgetName(widgetName).withGridColumns(12));
}
/*******************************************************************************
** The Jobs app and its home widgets require "<appName>.hasAccess"; users
** without it never see the app or its widgets (hidden, not locked).
*******************************************************************************/
private QPermissionRules appAccessRules()
{
return (new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)
.withPermissionBaseName(qBitConfig.getAppName()).withDenyBehavior(DenyBehavior.HIDDEN));
}
/*******************************************************************************
** Match the exact core permission, without a .hasAccess suffix.
*******************************************************************************/
@@ -0,0 +1,109 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2026. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.jobs;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/*******************************************************************************
** The Jobs app and its home widgets are hidden from users without
** "jobs.hasAccess"; Workers additionally needs jobs_admin.
*******************************************************************************/
class JobsAppPermissionTest extends BaseJobsTest
{
private static final List<String> NON_ADMIN_WIDGETS = JobsQBitProducer.HOME_WIDGETS.stream().filter(w -> !"jobsHomeWorkers".equals(w)).toList();
/*******************************************************************************
** The permission name is public and follows the configured app name.
*******************************************************************************/
@Test
void permissionNameAndRules()
{
assertThat(config.getHasAccessPermissionName()).isEqualTo("jobs.hasAccess");
assertThat(new JobsQBitConfig().withAppName("bg").getHasAccessPermissionName()).isEqualTo("bg.hasAccess");
assertThat(instance.getApp("jobs").getPermissionRules().getLevel()).isEqualTo(PermissionLevel.HAS_ACCESS_PERMISSION);
assertThat(instance.getApp("jobs").getPermissionRules().getPermissionBaseName()).isEqualTo("jobs");
assertThat(instance.getApp("jobs").getPermissionRules().getDenyBehavior()).isEqualTo(DenyBehavior.HIDDEN);
assertThat(instance.getWidget("jobsJobProgress").getPermissionRules().getLevel()).isNotEqualTo(PermissionLevel.HAS_ACCESS_PERMISSION);
for(String name : NON_ADMIN_WIDGETS)
{
assertThat(instance.getWidget(name).getPermissionRules().getPermissionBaseName()).isEqualTo("jobs");
assertThat(instance.getWidget(name).getPermissionRules().getDenyBehavior()).isEqualTo(DenyBehavior.HIDDEN);
}
}
/*******************************************************************************
** No permissions: no app, no home widgets, every render denied.
*******************************************************************************/
@Test
void noPermissionsSeesNothing() throws Exception
{
instance.setEnforcePermissionsInCoreActions(true);
login("nobody", false);
MetaDataOutput output = new MetaDataAction().execute(new MetaDataInput());
assertThat(output.getApps()).doesNotContainKey("jobs");
assertThat(output.getWidgets()).doesNotContainKeys(JobsQBitProducer.HOME_WIDGETS.toArray(String[]::new));
for(String name : JobsQBitProducer.HOME_WIDGETS)
{
assertThatThrownBy(() -> render(name, null)).isInstanceOf(QPermissionDeniedException.class);
}
}
/*******************************************************************************
** jobs.hasAccess: app and the five widgets, Workers only with jobs_admin.
*******************************************************************************/
@Test
void hasAccessSeesAppAndWidgets() throws Exception
{
instance.setEnforcePermissionsInCoreActions(true);
login("user", false).withPermission(config.getHasAccessPermissionName());
MetaDataOutput output = new MetaDataAction().execute(new MetaDataInput());
assertThat(output.getApps()).containsKey("jobs");
assertThat(output.getWidgets()).containsKeys(NON_ADMIN_WIDGETS.toArray(String[]::new));
assertThat(output.getWidgets()).doesNotContainKey("jobsHomeWorkers");
for(String name : NON_ADMIN_WIDGETS)
{
assertThat(render(name, null)).isNotNull();
}
assertThatThrownBy(() -> render("jobsHomeWorkers", null)).isInstanceOf(QPermissionDeniedException.class);
login("admin", true).withPermission(config.getHasAccessPermissionName());
assertThat(new MetaDataAction().execute(new MetaDataInput()).getWidgets()).containsKey("jobsHomeWorkers");
assertThat(render("jobsHomeWorkers", null)).isNotNull();
}
}
@@ -139,7 +139,7 @@ class JobsProgressTest extends BaseJobsTest
.withValue(PROGRESS_MESSAGE, "Uploading manifest").withValue(BYTES_DONE, 1536L).withValue(BYTES_TOTAL, 1048576L)
.withValue(ETA_SECONDS, 90L).withValue(LOG_DROPPED, 12)
.withValue(PROGRESS_JSON, "{\"phases\":[{\"name\":\"upload\",\"label\":\"Upload backup\"}]}"));
login(USER_A, false);
login(USER_A, false).withPermission(config.getHasAccessPermissionName());
TableData home = (TableData) render("jobsHomeActive", null);
assertThat(home.getRows()).singleElement().satisfies(row -> assertThat(row)
.containsEntry("Progress", "73%").containsEntry("Phase", "Upload backup")
@@ -148,7 +148,7 @@ class JobsProgressTest extends BaseJobsTest
MultiStatisticsData record = (MultiStatisticsData) render("jobsJobProgress", job.getValueString(ID));
assertThat(record.getStatisticsGroupData()).extracting(MultiStatisticsData.StatisticsGroupData::getSubheader)
.contains("Upload backup | Uploading manifest | 1.5 KiB / 1.0 MiB", "73% done", "ETA in 1m", "12 earlier lines not kept");
login(USER_B, false);
login(USER_B, false).withPermission(config.getHasAccessPermissionName());
assertThat(((TableData) render("jobsHomeActive", null)).getRows()).isEmpty();
assertThat(((MultiStatisticsData) render("jobsJobProgress", job.getValueString(ID))).getStatisticsGroupData()).isEmpty();
}