Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb7028a446
|
||
|
|
5e76611393 | ||
|
|
d1939eefd5 | ||
|
|
f4cbdae305 | ||
|
|
c0acf6bbf8 | ||
|
|
8846d0e514 | ||
|
|
7f77969f8e
|
||
|
|
33f1e8dcf4 | ||
|
|
318f176260
|
||
|
|
3adb6dc3d6 | ||
|
|
b89c95f749
|
||
|
|
86b4ca5ff4
|
||
|
|
a79ca3f696 | ||
|
|
c70210dee0
|
||
|
|
56e174444a |
No files matched your search
+11
-57
@@ -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.7.12
|
||||
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.7.12
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only: /^v\d+\.\d+\.\d+$/
|
||||
@@ -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@c288a8864fe9054f68410e1d78f5863aea4c4d22
|
||||
@@ -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@cc30f599e63c8e41629c99d23395bac1adb8bc44
|
||||
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
@@ -0,0 +1,16 @@
|
||||
profile: qbit
|
||||
orb_version: 0.7.12
|
||||
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
|
||||
@@ -5,6 +5,7 @@ All notable changes to this project are documented here. The format follows Keep
|
||||
## [0.1.0] - Unreleased
|
||||
|
||||
### Fixed
|
||||
- Under OAuth2, per-recipient checks (alert visibility for external channels, follower record access, audience sessions) never worked: they used token-less automated sessions, which OAuth2 rejects, so external sends fell back to the conservative rule. When the host declares the QQQ `alerts` service principal (`AlertsQBitConfig.withServicePrincipal`, default `alerts`), these checks run on the recipient's behalf through it: the recipient's current access intersected with the principal's. Alert recording and routing bookkeeping stay on the system session, so alerting does not depend on the identity provider being up. With no principal declared, behavior is unchanged. Needs the QQQ release with service identities (qqq#1255, #1256).
|
||||
- Audit noise: every monitor run (lease claim, then run result) and every reminder check wrote a "Record was Edited" audit (about 7,000 `alerts_monitor` and 1,800 `alerts_alert` audits in 6 hours on one production host, most with no field details). Engine updates that touch only bookkeeping fields (`RuntimeFields`: monitor `claimedBy`, `claimedUntil`, `lastRunAt`, `nextRunAt`, `lastStatus`, `lastValue`, `lastError`, `lastElapsedMs`, `consecutiveFailures`, `conditionSince`; alert `timedNotificationCheckedAt`, `lastNotifiedAt`, `recordRefCount`) are written with `omitDmlAudit`. Configuration edits, raises and alert state changes are audited as before, as are UI and API edits of any field.
|
||||
- Read the QBit metadata version from the Maven-filtered artifact version so release builds report their release version.
|
||||
- Record-view child lists (e.g. a notification's Recipients) failed with "Error in JSON Serialization": the child table's meta-data includes the qbit config, and `AlertsQBitConfig.getRuntime()` points at a runtime that points back at the config, so Jackson recursed to its 1000-level nesting limit. `getRuntime()` and `getAudienceResolver()` are now `@JsonIgnore`, as in the other qbits' configs. A regression test renders and serializes every child-list widget.
|
||||
|
||||
@@ -267,7 +267,7 @@
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<qqq.snapshot.version>4.1.0-RC.15</qqq.snapshot.version>
|
||||
<qqq.snapshot.version>4.1.0-RC.19</qqq.snapshot.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
@@ -80,6 +80,7 @@ public class AlertsQBitConfig implements QBitConfig
|
||||
private Boolean legacyAlertmanagerPath = false;
|
||||
|
||||
private String backendName;
|
||||
private String servicePrincipal = "alerts";
|
||||
private String schedulerName;
|
||||
private String messagingProviderName;
|
||||
private String fromAddress;
|
||||
@@ -893,4 +894,30 @@ public class AlertsQBitConfig implements QBitConfig
|
||||
this.legacyAlertmanagerPath = legacyAlertmanagerPath;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for servicePrincipal: when the host declares this QQQ service
|
||||
** principal, per-recipient checks (alert visibility, follower record access,
|
||||
** audience) run on the recipient's behalf through it - their current access
|
||||
** intersected with the principal's - instead of a token-less automated
|
||||
** session (rejected under OAuth2). Alert recording and routing bookkeeping
|
||||
** stay on the system session so alerting never depends on the identity
|
||||
** provider being up.
|
||||
*******************************************************************************/
|
||||
public String getServicePrincipal()
|
||||
{
|
||||
return (this.servicePrincipal);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for servicePrincipal (default alerts).
|
||||
*******************************************************************************/
|
||||
public AlertsQBitConfig withServicePrincipal(String servicePrincipal)
|
||||
{
|
||||
this.servicePrincipal = servicePrincipal;
|
||||
return (this);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
||||
import com.kingsrook.qqq.backend.core.context.CapturedContext;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
|
||||
@@ -34,6 +35,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.serviceidentity.ServiceIdentities;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
@@ -307,4 +311,25 @@ public final class AlertsSecurity
|
||||
{
|
||||
return (QContext.getQSession() instanceof QSystemUserSession);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A session for a recipient: on their behalf through the alerts service
|
||||
** principal when the host declares it (QQQ service identities), else the
|
||||
** authentication module's automated session (previous behavior). Null when
|
||||
** neither can be made - callers fail closed.
|
||||
*******************************************************************************/
|
||||
public static QSession recipientSession(QInstance qInstance, String principalName, String userId) throws QException
|
||||
{
|
||||
if(qInstance == null || qInstance.getAuthentication() == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
if(StringUtils.hasContent(principalName) && ServiceIdentities.isDeclared(qInstance, principalName))
|
||||
{
|
||||
return (ServiceIdentities.sessionOnBehalfOf(qInstance, principalName, userId));
|
||||
}
|
||||
QAuthenticationModuleInterface module = new QAuthenticationModuleDispatcher().getQModule(qInstance.getAuthentication());
|
||||
return (module.createAutomatedSessionForUser(qInstance, userId));
|
||||
}
|
||||
}
|
||||
@@ -66,8 +66,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.collaboration.FollowEvent;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.collaboration.RecordFollower;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.collaboration.RecordReference;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
@@ -882,18 +880,12 @@ public class AlertRouter
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A session for a user, from the instance's authentication module (null
|
||||
** when it can't make one).
|
||||
** A session for a user: on their behalf via the alerts service principal
|
||||
** when declared, else the authentication module's (null when neither).
|
||||
*******************************************************************************/
|
||||
private QSession automatedSession(String userId) throws QException
|
||||
{
|
||||
QInstance qInstance = runtime.getQInstance();
|
||||
if(qInstance.getAuthentication() == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
QAuthenticationModuleInterface module = new QAuthenticationModuleDispatcher().getQModule(qInstance.getAuthentication());
|
||||
return (module.createAutomatedSessionForUser(qInstance, userId));
|
||||
return (AlertsSecurity.recipientSession(runtime.getQInstance(), runtime.getConfig().getServicePrincipal(), userId));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qbits.alerts.AlertsRuntime;
|
||||
import com.kingsrook.qbits.alerts.AlertsSecurity;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.alerts.NotificationAudience;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@@ -33,8 +34,6 @@ import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@@ -218,20 +217,14 @@ public class AlertsAudienceResolver
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A session for a user from the instance's authentication module (null
|
||||
** when it can't make one).
|
||||
** A session for a user: on their behalf via the alerts service principal
|
||||
** when declared, else the authentication module's (null when neither).
|
||||
*******************************************************************************/
|
||||
private QSession automatedSession(String userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
QInstance qInstance = runtime.getQInstance();
|
||||
if(qInstance.getAuthentication() == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
QAuthenticationModuleInterface module = new QAuthenticationModuleDispatcher().getQModule(qInstance.getAuthentication());
|
||||
return (module.createAutomatedSessionForUser(qInstance, userId));
|
||||
return (AlertsSecurity.recipientSession(runtime.getQInstance(), runtime.getConfig().getServicePrincipal(), userId));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.alerts;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.serviceidentity.QDelegatedSession;
|
||||
import com.kingsrook.qqq.backend.core.serviceidentity.QServicePrincipalMetaData;
|
||||
import com.kingsrook.qqq.backend.core.serviceidentity.ServiceIdentities;
|
||||
import com.kingsrook.qqq.backend.core.serviceidentity.ServiceIdentitiesMetaData;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Per-recipient checks run on the recipient's behalf through the alerts
|
||||
** service principal when the host declares it; otherwise unchanged.
|
||||
*******************************************************************************/
|
||||
class AlertsServicePrincipalTest extends BaseAlertsTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void recipientSessionsGoThroughTheDeclaredPrincipal() throws Exception
|
||||
{
|
||||
QSession before = AlertsSecurity.recipientSession(qInstance, config.getServicePrincipal(), "alice");
|
||||
assertThat(before).isNotInstanceOf(QDelegatedSession.class);
|
||||
assertThat(before.getUser().getIdReference()).isEqualTo("alice");
|
||||
|
||||
ServiceIdentitiesMetaData.declare(qInstance, new QServicePrincipalMetaData().withName("alerts").withMayActOnBehalfOfUsers(true));
|
||||
QSession after = AlertsSecurity.recipientSession(qInstance, config.getServicePrincipal(), "alice");
|
||||
assertThat(after).isInstanceOf(QDelegatedSession.class);
|
||||
assertThat(after.getUser().getIdReference()).isEqualTo("alice");
|
||||
assertThat(ServiceIdentities.principalOf(after)).isEqualTo("alerts");
|
||||
assertThat(ServiceIdentities.attributionName(after)).endsWith("via service:alerts");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user