You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@activemq.apache.org by jb...@apache.org on 2025/11/18 20:20:42 UTC
(activemq-artemis-console) 06/08: ARTEMIS-5743: Handle inaccessible attributes better, when no RBAC metadata is available
This is an automated email from the ASF dual-hosted git repository.
jbertram pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/activemq-artemis-console.git
commit 904ca3887a4ae647183461d555dc27511902ac28
Author: Grzegorz Grzybek <gr...@gmail.com>
AuthorDate: Tue Nov 18 11:42:00 2025 +0100
ARTEMIS-5743: Handle inaccessible attributes better, when no RBAC metadata is available
---
.../src/artemis-service.test.ts | 11 +-
.../artemis-console-plugin/src/artemis-service.ts | 206 ++++++++++++++++++---
.../artemis-console-plugin/src/status/Status.tsx | 146 +++++++++------
3 files changed, 282 insertions(+), 81 deletions(-)
diff --git a/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/artemis-service.test.ts b/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/artemis-service.test.ts
index 7ff962e..d043919 100644
--- a/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/artemis-service.test.ts
+++ b/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/artemis-service.test.ts
@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { beforeAll, describe, expect, test } from "@jest/globals"
-import { artemisService } from "./artemis-service";
+import { artemisService, parseMBeanName } from "./artemis-service";
import { SortDirection } from './table/ArtemisTable'
import { userService } from '@hawtio/react'
@@ -41,4 +41,13 @@ describe("Artemis Service basic tests", () => {
await expect(addresses).resolves.toContain("DLQ");
})
+ test("Splitting ObjectNames", () => {
+ const mbean = "org.apache.activemq.artemis:broker=\"0.0.0.0:61616\",component=acceptors,filter=\"x,y,z=a\",name=amqp"
+ const parsed = parseMBeanName(mbean)
+ expect(parsed.domain).toEqual("org.apache.activemq.artemis")
+ expect(parsed.properties["broker"]).toEqual("\"0.0.0.0:61616\"")
+ expect(parsed.properties["filter"]).toEqual("\"x,y,z=a\"")
+ expect(parsed.properties["name"]).toEqual("amqp")
+ })
+
})
diff --git a/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/artemis-service.ts b/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/artemis-service.ts
index c25e5b1..aee3de3 100644
--- a/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/artemis-service.ts
+++ b/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/artemis-service.ts
@@ -15,23 +15,40 @@
* limitations under the License.
*/
import { ActiveSort, Filter } from './table/ArtemisTable'
-import { jolokiaService, MBeanNode } from '@hawtio/react'
+import { eventService, jolokiaService, MBeanNode, workspace } from '@hawtio/react'
import { createAddressObjectName, createQueueObjectName } from './util/jmx'
import { log } from './globals'
import { Message } from './messages/MessageView'
import { configManager } from './config-manager'
+export type BrokerState = {
+ // loading attempted?
+ loaded: boolean
+ // permission granted?
+ accessible: boolean
+ // possible error message
+ message?: string
+ // if everything is configured correctly, this is fully populated broker information
+ info?: BrokerInfo
+}
+// Value returned by Jolokia, when RBAC prevents access
+// see: org.jolokia.server.core.service.serializer.ValueFaultHandler.IGNORING_VALUE_FAULT_HANDLER
+export type InaccessibleValue = {
+ ".error": boolean
+ error?: string
+ error_type?: string
+}
export type BrokerInfo = {
name: string
nodeID: string
objectName: string
- version: string
- started: string
- uptime: string
+ version: string | InaccessibleValue
+ started: string | InaccessibleValue
+ uptime: string | InaccessibleValue
globalMaxSizeMB: number
addressMemoryUsage: number
addressMemoryUsed: number
- haPolicy: string
+ haPolicy: string | InaccessibleValue
networkTopology: BrokerNetworkTopology
}
@@ -179,28 +196,55 @@ class ArtemisService {
const brokerObjectName = await this.brokerObjectName;
const response = await jolokiaService.readAttribute(brokerObjectName, "Name");
if (response) {
+ if (typeof response === "object" && ".error" in response) {
+ return ""
+ }
return response as string;
}
return null;
}
- async getBrokerInfo(): Promise<BrokerInfo | null> {
- return new Promise<BrokerInfo | null>(async (resolve, reject) => {
+ async getBrokerInfo(): Promise<BrokerState> {
+ return new Promise<BrokerState>(async (resolve, _reject) => {
const brokerObjectName = await this.brokerObjectName;
if ("" === brokerObjectName) {
- resolve(null)
+ resolve({ loaded: true, accessible: false, message: "No Broker ObjectName available" })
return
}
- const response = await jolokiaService.readAttributes(brokerObjectName).catch(e => null);
+ const brokerMBean = await findMBeanInfo(brokerObjectName)
+ if (!brokerMBean) {
+ resolve({ loaded: true, accessible: false, message: `No Broker MBean available for name ${brokerObjectName}` })
+ return
+ }
+ // `canRead` flag is added by jolokia-integration 0.7.1+ and ideally it should be checked _before_
+ // getting an attribute (just as `canInvoke` should be checked before executing an operation), but
+ // RBAC data may not be complete, so it's still good to be prepared (and add relevant `.catch(error => {...})`
+ let permitted = false
+ const nameInfo = brokerMBean.mbean?.attr?.["Name"]
+ if (nameInfo && "canRead" in nameInfo) {
+ permitted = nameInfo["canRead"] as boolean
+ } else if ("canInvoke" in brokerMBean.mbean!) {
+ permitted = brokerMBean.mbean?.["canInvoke"] as boolean
+ }
+ if (!permitted) {
+ resolve({ loaded: true, accessible: false, message: `Access not granted to broker ${brokerMBean.objectName}` })
+ return
+ }
+
+ const response = await jolokiaService.readAttributes(brokerObjectName).catch(e => {
+ // this is the best (as of Nov 2025) way to handle problems when fetching attributes with RBAC enabled
+ eventService.notify({type: 'warning', message: jolokiaService.errorMessage(e) })
+ return null
+ });
if (response) {
const name = response.Name as string;
const nodeID = response.NodeID as string;
- const version = response.Version as string;
- const started = "" + response.Started as string;
+ const version = response.Version as string | InaccessibleValue;
+ const started = response.Started as string | InaccessibleValue;
const globalMaxSize = response.GlobalMaxSize as number;
const addressMemoryUsage = response.AddressMemoryUsage as number;
- const uptime = response.Uptime as string;
- const haPolicy = response.HAPolicy as string;
+ const uptime = response.Uptime as string | InaccessibleValue;
+ const haPolicy = response.HAPolicy as string | InaccessibleValue;
const globalMaxSizeMB = globalMaxSize / 1048576;
let used = 0;
let addressMemoryUsageMB = 0;
@@ -208,9 +252,13 @@ class ArtemisService {
addressMemoryUsageMB = addressMemoryUsage / 1048576;
used = addressMemoryUsageMB / globalMaxSizeMB * 100
}
- const topology = await jolokiaService.execute(brokerObjectName, LIST_NETWORK_TOPOLOGY_SIG) as string;
+ const topology = await jolokiaService.execute(brokerObjectName, LIST_NETWORK_TOPOLOGY_SIG).catch(e => {
+ eventService.notify({type: 'warning', message: jolokiaService.errorMessage(e) })
+ return "{}"
+ }) as string;
const brokerInfo: BrokerInfo = {
- name: name, objectName: brokerObjectName,
+ name: name,
+ objectName: brokerObjectName,
nodeID: nodeID,
version: version,
started: started,
@@ -221,18 +269,26 @@ class ArtemisService {
haPolicy: haPolicy,
networkTopology: new BrokerNetworkTopology(JSON.parse(topology))
};
- resolve(brokerInfo);
+ resolve({ loaded: true, accessible: true, info: brokerInfo });
+ return
}
- resolve(null)
+ resolve({ loaded: true, accessible: false, message: `No information available for broker ${brokerMBean.objectName}` })
});
}
- async createBrokerTopology(maxAddresses: number, addressFilter: string): Promise<BrokerTopology> {
- return new Promise<BrokerTopology>(async (resolve, reject) => {
+ async createBrokerTopology(maxAddresses: number, addressFilter: string): Promise<BrokerTopology | null> {
+ return new Promise<BrokerTopology | null>(async (resolve, reject) => {
try {
- const brokerInfo = await this.getBrokerInfo();
+ const state = await this.getBrokerInfo();
+ if (!state || !state.info) {
+ resolve(null)
+ }
+ const brokerInfo = state.info
const brokerObjectName = await this.brokerObjectName;
- const topology = await jolokiaService.execute(brokerObjectName, LIST_NETWORK_TOPOLOGY_SIG) as string;
+ const topology = await jolokiaService.execute(brokerObjectName, LIST_NETWORK_TOPOLOGY_SIG).catch(error => {
+ eventService.notify({ type: "warning", message: jolokiaService.errorMessage(error) })
+ return "[]"
+ }) as string;
brokerInfo!.networkTopology = new BrokerNetworkTopology(JSON.parse(topology));
const brokerTopology: BrokerTopology = {
broker: brokerInfo!,
@@ -242,7 +298,10 @@ class ArtemisService {
const max: number = maxAddresses < addresses.length ? maxAddresses: addresses.length;
addresses = addresses.slice(0, max);
for (const address of addresses) {
- const queuesJson: string = await this.getQueuesForAddress(address);
+ const queuesJson: string = await this.getQueuesForAddress(address).catch(error => {
+ eventService.notify({ type: "warning", message: jolokiaService.errorMessage(error) })
+ return JSON.stringify({ data: [], count: 0 })
+ })
const queues: Queue[] = JSON.parse(queuesJson).data;
brokerTopology.addresses.push({
name: address,
@@ -273,7 +332,12 @@ class ArtemisService {
.catch((e) => {
reject(e)
}) as Acceptor;
- acceptors.acceptors.push(acceptor);
+ const validation = isValid(acceptor)
+ if (validation.valid) {
+ acceptors.acceptors.push(acceptor);
+ } else {
+ eventService.notify({ type: "warning", message: `Access not granted to ${search[key]}` })
+ }
}
resolve(acceptors);
}
@@ -296,7 +360,12 @@ class ArtemisService {
.catch((e) => {
reject(e)
}) as ClusterConnection;
- clusterConnections.clusterConnections.push(clusterConnection);
+ const validation = isValid(clusterConnection)
+ if (validation.valid) {
+ clusterConnections.clusterConnections.push(clusterConnection);
+ } else {
+ eventService.notify({ type: "warning", message: `Access not granted to ${search[key]}` })
+ }
}
resolve(clusterConnections);
}
@@ -691,4 +760,93 @@ class ArtemisService {
}
}
+export function parseMBeanName(name: string): { domain: string, properties: Record<string, string> } {
+ const colon = name.indexOf(":")
+ if (colon === -1) {
+ throw new Error("Illegal ObjectName")
+ }
+
+ const domain = name.substring(0, colon)
+ const propsString = name.substring(colon + 1)
+
+ let i = 0
+ const len = propsString.length
+ const props: Record<string, string> = {}
+
+ while (i < len) {
+ let key = ""
+ while (i < len && propsString[i] !== "=") {
+ key += propsString[i++]
+ }
+
+ // skip '='
+ i++
+
+ let value = ""
+ if (propsString[i] === '"') {
+ // quoted value - only double quote
+ i++
+ while (i < len) {
+ const ch = propsString[i++]
+ if (ch === '"') {
+ break
+ }
+ value += ch
+ }
+ } else {
+ // unquoted value can be empty
+ while (i < len && propsString[i] !== ",") {
+ value += propsString[i++]
+ }
+ }
+
+ props[key.trim()] = value.trim()
+
+ if (propsString[i] === ",") {
+ i++
+ }
+ }
+
+ return { domain, properties: props };
+}
+
+export async function findMBeanInfo(brokerObjectName: string): Promise<MBeanNode | null> {
+ const tree = await workspace.getTree()
+ const parsed = parseMBeanName(brokerObjectName)
+
+ const matching = tree.findMBeans(parsed.domain, parsed.properties)
+ if (!matching) {
+ return null
+ }
+ if (matching.length == 1) {
+ return matching[0]
+ } else {
+ const more = matching.filter(node => node.objectName === brokerObjectName)
+ return more.length > 0 ? more[0] : null
+ }
+}
+
+/**
+ * Jolokia's `getAttributes()` may return an object, where each field is an error value (when RBAC is enabled
+ * for example). In this case, the value has special `.error` field. This function checks validity of an object
+ * or value.
+ * @param value
+ */
+export function isValid(value: any): { valid: boolean, message: string | null } {
+ if (typeof value === "object") {
+ if (".error" in value) {
+ // the value itself is an error value
+ return { valid: false, message: jolokiaService.errorMessage(value) }
+ }
+ for (const k in value) {
+ const v = value[k]
+ if (typeof v === "object" && ".error" in v) {
+ // fail fast with an error related to first field of the wrapping object
+ return { valid: false, message: jolokiaService.errorMessage(v) }
+ }
+ }
+ }
+ return { valid: true, message: null }
+}
+
export const artemisService = new ArtemisService()
\ No newline at end of file
diff --git a/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/status/Status.tsx b/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/status/Status.tsx
index a250f98..3578bc3 100644
--- a/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/status/Status.tsx
+++ b/artemis-console-extension/artemis-extension/packages/artemis-console-plugin/src/status/Status.tsx
@@ -16,6 +16,7 @@
*/
import { ChartDonutUtilization } from "@patternfly/react-charts"
import {
+ Alert,
Card,
CardBody,
CardTitle,
@@ -34,20 +35,25 @@ import {
Modal,
ModalVariant,
DropdownList,
+ MenuToggle,
MenuToggleElement,
- MenuToggle
+ PageSection,
+ Spinner,
+ Tooltip
} from "@patternfly/react-core"
import { EllipsisVIcon } from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'
import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'
import { OkIcon } from '@patternfly/react-icons/dist/esm/icons/ok-icon'
-import { Attributes, eventService, Operations } from '@hawtio/react';
-import React, { useContext, useEffect, useState } from "react";
-import { Acceptors, artemisService, BrokerInfo, ClusterConnections } from "../artemis-service";
+import { Attributes, eventService, jolokiaService, Operations } from '@hawtio/react';
+import React, { ReactNode, useContext, useEffect, useState } from "react";
+import { Acceptors, artemisService, BrokerInfo, BrokerState, ClusterConnections } from "../artemis-service";
import { ArtemisContext } from "../context";
import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table";
+import { LockedIcon } from '@patternfly/react-icons'
export const Status: React.FunctionComponent = () => {
+ const [brokerState, setBrokerState] = useState<BrokerState>({ loaded: false, accessible: false, message: "Loading..." })
const [brokerInfo, setBrokerInfo] = useState<BrokerInfo>()
const [acceptors, setAcceptors] = useState<Acceptors>();
const [clusterConnections, setClusterConnections] = useState<ClusterConnections>()
@@ -59,13 +65,14 @@ export const Status: React.FunctionComponent = () => {
const getBrokerInfo = async () => {
artemisService.getBrokerInfo()
.then((brokerInfo) => {
- setBrokerInfo(brokerInfo ?? undefined)
+ if (brokerInfo.info) {
+ setBrokerInfo(brokerInfo.info)
+ }
+ setBrokerState({ loaded: brokerInfo.loaded, accessible: brokerInfo.accessible, message: brokerInfo.message })
})
- .catch((error: string | { [key: string]: any }) => {
- eventService.notify({
- type: 'warning',
- message: typeof error === 'object' ? ('error' in error ? error.error : JSON.stringify(error)) : error,
- })
+ .catch((error) => {
+ setBrokerState({ loaded: false, accessible: false, message: `Error loading broker: ${jolokiaService.errorMessage(error)}` })
+ eventService.notify({type: 'warning', message: jolokiaService.errorMessage(error) })
});
}
@@ -74,11 +81,8 @@ export const Status: React.FunctionComponent = () => {
.then((acceptors) => {
setAcceptors(acceptors)
})
- .catch((error: string | { [key: string]: any }) => {
- eventService.notify({
- type: 'warning',
- message: typeof error === 'object' ? ('error' in error ? error.error : JSON.stringify(error)) : error,
- })
+ .catch((error) => {
+ eventService.notify({type: 'warning', message: jolokiaService.errorMessage(error) })
});
}
@@ -87,11 +91,8 @@ export const Status: React.FunctionComponent = () => {
.then((clusterConnections) => {
setClusterConnections(clusterConnections)
})
- .catch((error: string | { [key: string]: any }) => {
- eventService.notify({
- type: 'warning',
- message: typeof error === 'object' ? ('error' in error ? error.error : JSON.stringify(error)) : error,
- })
+ .catch((error) => {
+ eventService.notify({type: 'warning', message: jolokiaService.errorMessage(error) })
});
}
@@ -118,7 +119,7 @@ export const Status: React.FunctionComponent = () => {
setIsBrokerInfoOpen(!isBrokerInfoOpen);
};
- const openAttrubutes = async () => {
+ const openAttributes = async () => {
const brokerObjectName = await artemisService.getBrokerObjectName();
findAndSelectNode(brokerObjectName, "");
setShowAttributesDialog(true);
@@ -144,7 +145,7 @@ export const Status: React.FunctionComponent = () => {
isOpen={isBrokerInfoOpen}
>
<DropdownList>
- <DropdownItem key="attributes" component="button" onClick={() => openAttrubutes()}>
+ <DropdownItem key="attributes" component="button" onClick={() => openAttributes()}>
Attributes
</DropdownItem>
<DropdownItem key="operations" component="button" onClick={() => openOperations()}>
@@ -154,6 +155,31 @@ export const Status: React.FunctionComponent = () => {
</Dropdown>
);
+ function actualValue(v: unknown): ReactNode {
+ if (typeof v === 'object' && v && (".error" in v) && v[".error"]) {
+ const msg = "error" in v ? v["error"] as string : "Not accessible"
+ return (
+ <Tooltip content={msg}>
+ <LockedIcon />
+ </Tooltip>
+ )
+ } else {
+ return (v ? "" + v : "")
+ }
+ }
+
+ if (!brokerState.loaded) {
+ return <Spinner size='lg' />
+ }
+
+ if (!brokerState.accessible) {
+ return (
+ <PageSection id='broker-info' variant='light'>
+ <Alert variant="warning" title={brokerState.message} />
+ </PageSection>
+ )
+ }
+
return (
<>
<Grid hasGutter>
@@ -166,10 +192,10 @@ export const Status: React.FunctionComponent = () => {
<Divider />
<TextContent>
<TextList isPlain>
- <TextListItem component={TextListItemVariants.dd}>Version: {brokerInfo?.version}</TextListItem>
- <TextListItem component={TextListItemVariants.dd}>Uptime: {brokerInfo?.uptime}</TextListItem>
- <TextListItem component={TextListItemVariants.dd}>Started: {""+brokerInfo?.started}</TextListItem>
- <TextListItem component={TextListItemVariants.dd}>HA Policy: {brokerInfo?.haPolicy}</TextListItem>
+ <TextListItem component={TextListItemVariants.dd}>Version: {actualValue(brokerInfo?.version)}</TextListItem>
+ <TextListItem component={TextListItemVariants.dd}>Uptime: {actualValue(brokerInfo?.uptime)}</TextListItem>
+ <TextListItem component={TextListItemVariants.dd}>Started: {actualValue(brokerInfo?.started)}</TextListItem>
+ <TextListItem component={TextListItemVariants.dd}>HA Policy: {actualValue(brokerInfo?.haPolicy)}</TextListItem>
</TextList>
</TextContent>
</CardBody>
@@ -205,38 +231,46 @@ export const Status: React.FunctionComponent = () => {
<ExpandableSection toggleTextExpanded="Acceptors" toggleTextCollapsed="Acceptors">
<Grid hasGutter span={4}>
{
- acceptors?.acceptors.map((acceptor, index) => (
- <GridItem key={index}>
- <Card isFullHeight={true} isFlat={true}>
+ acceptors?.acceptors.map((acceptor, index) => {
+ if (!(typeof acceptor.Name === 'object' && ".error" in acceptor.Name)) {
+ return (
+ <GridItem key={index}>
+ <Card isFullHeight={true} isFlat={true}>
- <CardTitle>{acceptor.Name} ({acceptor.FactoryClassName.indexOf("Netty") === -1?"VM":"TCP"}): {acceptor.Started && <OkIcon color="green" />}{!acceptor.Started && <ExclamationCircleIcon color="red"/>}</CardTitle>
- <CardBody>
- <Divider />
- <Table variant="compact" aria-label="Column Management Table">
- <Thead>
- <Tr key={"acceptor-list-param-title"}>
- <Th key={"acceptor-list-param-key" + index}>key</Th>
- <Th key={"acceptor-list-param-value" + index}>value</Th>
- </Tr>
- </Thead>
- <Tbody>
- {
- Object.keys(acceptor.Parameters).map((key, index) => {
- return (
- <Tr key={"acceptor-list-param-val-" + index}>
- <Td key={"acceptor-params-key-" + key}>{key}</Td>
- <Td key={"acceptor-params-val-" + key}>{acceptor.Parameters[key]}</Td>
+ <CardTitle>{acceptor.Name} ({typeof acceptor?.FactoryClassName === 'string' ? (acceptor?.FactoryClassName?.indexOf("Netty") === -1 ? "VM" : "TCP") : "?"}): {acceptor.Started &&
+ <OkIcon color="green" />}{!acceptor.Started &&
+ <ExclamationCircleIcon color="red" />}</CardTitle>
+ <CardBody>
+ <Divider />
+ <Table variant="compact" aria-label="Column Management Table">
+ <Thead>
+ <Tr key={"acceptor-list-param-title"}>
+ <Th key={"acceptor-list-param-key" + index}>key</Th>
+ <Th key={"acceptor-list-param-value" + index}>value</Th>
</Tr>
+ </Thead>
+ <Tbody>
+ {
+ Object.keys(acceptor.Parameters).map((key, index) => {
+ return (
+ <Tr key={"acceptor-list-param-val-" + index}>
+ <Td key={"acceptor-params-key-" + key}>{key}</Td>
+ <Td key={"acceptor-params-val-" + key}>{acceptor.Parameters[key]}</Td>
+ </Tr>
- )
- })
- }
- </Tbody>
- </Table>
- </CardBody>
- </Card>
- </GridItem>
- ))
+ )
+ })
+ }
+ </Tbody>
+ </Table>
+ </CardBody>
+ </Card>
+ </GridItem>
+ )} else {
+ // return (<span>{acceptor.Name["message"]}</span>)
+ return null
+ }
+ })
}
</Grid>
</ExpandableSection>
@@ -282,7 +316,7 @@ export const Status: React.FunctionComponent = () => {
</Grid>
<Grid hasGutter>
{
- brokerInfo?.networkTopology.brokers.map((broker, index) => (
+ brokerInfo?.networkTopology?.brokers?.map?.((broker, index) => (
<GridItem key={index} span={3}>
<Card isFlat={true}>
<CardTitle>{broker.nodeID}</CardTitle>
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@activemq.apache.org
For additional commands, e-mail: commits-help@activemq.apache.org
For further information, visit: https://activemq.apache.org/contact