Skip to content

Commit

Permalink
feat(jmx): support writing attributes for writable attributes (RW)
Browse files Browse the repository at this point in the history
Fix #408
  • Loading branch information
tadayosi committed Oct 24, 2023
1 parent 13373ed commit 2dab258
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 50 deletions.
2 changes: 1 addition & 1 deletion packages/hawtio/src/plugins/rbac/rbac-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('RBACService', () => {
userService.isLogin = jest.fn(async () => true)
jolokiaService.search = jest.fn(async () => [])

await expect(rbacService.getACLMBean()).resolves.toBe('')
await expect(rbacService.getACLMBean()).resolves.toBeNull()
})

test('there is one ACLMBean', async () => {
Expand Down
12 changes: 6 additions & 6 deletions packages/hawtio/src/plugins/rbac/rbac-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ import { log } from './globals'
const ACL_MBEAN_PATTERN = '*:type=security,area=jmx,*'

interface IRBACService {
getACLMBean(): Promise<string>
reset(): void
getACLMBean(): Promise<string | null>
}

class RBACService implements IRBACService {
private aclMBean?: Promise<string>
private aclMBean?: Promise<string | null>

reset() {
this.aclMBean = undefined
}

getACLMBean(): Promise<string> {
getACLMBean(): Promise<string | null> {
if (this.aclMBean) {
return this.aclMBean
}
Expand All @@ -27,7 +27,7 @@ class RBACService implements IRBACService {
return this.aclMBean
}

private async fetchACLMBean(): Promise<string> {
private async fetchACLMBean(): Promise<string | null> {
if (!(await userService.isLogin())) {
throw new Error('User needs to have logged in to run RBAC plugin')
}
Expand All @@ -37,7 +37,7 @@ class RBACService implements IRBACService {

if (mbeans.length === 0) {
log.info("Didn't discover any ACL MBeans; client-side RBAC is disabled")
return ''
return null
}

const mbean = mbeans[0]
Expand All @@ -50,7 +50,7 @@ class RBACService implements IRBACService {
const chosen = mbeans.find(mbean => !mbean.includes('HawtioDummy'))
if (!chosen || isBlank(chosen)) {
log.info("Didn't discover any effective ACL MBeans; client-side RBAC is disabled")
return ''
return null
}
log.info('Use MBean', chosen, 'for client-side RBAC')
return chosen
Expand Down
3 changes: 1 addition & 2 deletions packages/hawtio/src/plugins/rbac/tree-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
} from '@hawtiosrc/plugins/shared'
import { operationToString } from '@hawtiosrc/util/jolokia'
import { isString } from '@hawtiosrc/util/objects'
import { isBlank } from '@hawtiosrc/util/strings'
import { Request, Response } from 'jolokia.js'
import { log } from './globals'
import { rbacService } from './rbac-service'
Expand Down Expand Up @@ -36,7 +35,7 @@ export const rbacTreeProcessor: TreeProcessor = async (tree: MBeanTree) => {
log.debug('Processing tree:', tree)
const aclMBean = await rbacService.getACLMBean()

if (isBlank(aclMBean)) {
if (!aclMBean) {
/*
* Some implementations of jolokia provision, eg. running with java -javaagent
* do not provide an acl mbean or implement server-side RBAC so need to skip
Expand Down
117 changes: 79 additions & 38 deletions packages/hawtio/src/plugins/shared/attributes/AttributeModal.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,103 @@
import {
Button,
ClipboardCopy,
Form,
FormGroup,
Modal,
ModalVariant,
TextArea,
TextInput,
} from '@patternfly/react-core'
import React, { useEffect, useState, useContext } from 'react'
import { attributeService } from './attribute-service'
import { PluginNodeSelectionContext } from '@hawtiosrc/plugins/context'
import { Button, ClipboardCopy, Form, FormGroup, Modal, TextArea, TextInput } from '@patternfly/react-core'
import React, { useContext, useEffect, useState } from 'react'
import { attributeService } from './attribute-service'
import { log } from '../globals'
import { eventService } from '@hawtiosrc/core'

export interface AttributeModalProps {
export const AttributeModal: React.FunctionComponent<{
isOpen: boolean
onClose: () => void
onUpdate: () => void
input: { name: string; value: string }
}

export const AttributeModal: React.FunctionComponent<AttributeModalProps> = props => {
}> = ({ isOpen, onClose, onUpdate, input }) => {
const { selectedNode } = useContext(PluginNodeSelectionContext)
const { isOpen, onClose, input } = props
const { name, value } = input
const attributeName = input.name
const [attributeValue, setAttributeValue] = useState('')
const [jolokiaUrl, setJolokiaUrl] = useState('Loading...')
const [isWritable, setIsWritable] = useState(false)

useEffect(() => {
if (!selectedNode || !selectedNode.objectName) {
if (!selectedNode || !selectedNode.objectName || !selectedNode.mbean) {
return
}

const mbean = selectedNode.objectName
const { mbean, objectName } = selectedNode

const attribute = mbean.attr?.[attributeName]
if (!attribute) {
return
}

setAttributeValue(input.value)

// Update Jolokia URL
const buildUrl = async () => {
const url = await attributeService.buildUrl(mbean, name)
const url = await attributeService.buildUrl(objectName, attributeName)
setJolokiaUrl(url)
}
buildUrl()
}, [selectedNode, name])

if (!selectedNode || !selectedNode.mbean || !selectedNode.objectName) {
// Check RBAC on the selected attribute
if (attribute.rw) {
// For writable attribute, we need to check RBAC
const canInvoke = async () => {
const canInvoke = await attributeService.canInvoke(objectName, attributeName, attribute.type)
log.debug('Attribute', attributeName, 'canInvoke:', canInvoke)
setIsWritable(canInvoke)
}
canInvoke()
} else {
setIsWritable(false)
}
}, [selectedNode, attributeName, input])

if (!selectedNode || !selectedNode.objectName || !selectedNode.mbean) {
return null
}

const attribute = selectedNode.mbean.attr?.[name]
const { mbean, objectName } = selectedNode

const attribute = mbean.attr?.[attributeName]
if (!attribute) {
return null
}

const modalTitle = `Attribute: ${input.name}`
const updateAttribute = async () => {
if (attributeValue === input.value) {
eventService.notify({ type: 'info', message: 'The attribute value has not changed' })
} else {
await attributeService.update(objectName, attributeName, attributeValue)
onUpdate()
}
onClose()
}

const modalTitle = `Attribute: ${attributeName}`

const modalActions = [
<Button key='close' variant='primary' onClick={onClose}>
Close
</Button>,
]
if (isWritable) {
modalActions.push(
<Button key='update' variant='danger' onClick={updateAttribute}>
Update
</Button>,
)
}

return (
<Modal
variant={ModalVariant.medium}
title={modalTitle}
isOpen={isOpen}
onClose={onClose}
actions={[
<Button key='close' onClick={onClose}>
Close
</Button>,
]}
>
<Modal variant='medium' title={modalTitle} isOpen={isOpen} onClose={onClose} actions={modalActions}>
<Form id='attribute-form' isHorizontal>
<FormGroup label='Name' fieldId='attribute-form-name'>
<TextInput id='attribute-form-name' name='attribute-form-name' value={name} readOnlyVariant='default' />
<TextInput
id='attribute-form-name'
name='attribute-form-name'
value={attributeName}
readOnlyVariant='default'
/>
</FormGroup>
<FormGroup label='Description' fieldId='attribute-form-description'>
<TextArea
Expand All @@ -86,7 +121,13 @@ export const AttributeModal: React.FunctionComponent<AttributeModalProps> = prop
</ClipboardCopy>
</FormGroup>
<FormGroup label='Value' fieldId='attribute-form-value'>
<TextInput id='attribute-form-value' name='attribute-form-value' value={value} readOnlyVariant='default' />
<TextInput
id='attribute-form-value'
name='attribute-form-value'
value={attributeValue}
onChange={value => setAttributeValue(value)}
readOnlyVariant={isWritable ? undefined : 'default'}
/>
</FormGroup>
</Form>
</Modal>
Expand Down
14 changes: 11 additions & 3 deletions packages/hawtio/src/plugins/shared/attributes/Attributes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ export const Attributes: React.FunctionComponent = () => {
const [isReading, setIsReading] = useState(true)
const [isModalOpen, setIsModalOpen] = useState(false)
const [selected, setSelected] = useState({ name: '', value: '' })
const [reload, setReload] = useState(true)

useEffect(() => {
if (!selectedNode || !selectedNode.mbean || !selectedNode.objectName) {
if (!selectedNode || !selectedNode.mbean || !selectedNode.objectName || !reload) {
return
}

Expand All @@ -31,7 +32,9 @@ export const Attributes: React.FunctionComponent = () => {
setIsReading(false)
}
readAttributes()
}, [selectedNode])

setReload(false)
}, [selectedNode, reload])

useEffect(() => {
if (!selectedNode || !selectedNode.mbean || !selectedNode.objectName) {
Expand Down Expand Up @@ -82,7 +85,12 @@ export const Attributes: React.FunctionComponent = () => {
<TableHeader />
<TableBody onRowClick={selectAttribute} />
</Table>
<AttributeModal isOpen={isModalOpen} onClose={handleModalToggle} input={selected} />
<AttributeModal
isOpen={isModalOpen}
onClose={handleModalToggle}
onUpdate={() => setReload(true)}
input={selected}
/>
</Card>
)
}
19 changes: 19 additions & 0 deletions packages/hawtio/src/plugins/shared/attributes/attribute-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { AttributeValues, jolokiaService } from '@hawtiosrc/plugins/shared/jolok
import { escapeMBean } from '@hawtiosrc/util/jolokia'
import { Request, Response } from 'jolokia.js'
import { log } from '../globals'
import { rbacService } from '@hawtiosrc/plugins/rbac/rbac-service'
import { eventService } from '@hawtiosrc/core'

class AttributeService {
private handles: number[] = []
Expand All @@ -26,6 +28,23 @@ class AttributeService {
const jolokiaUrl = await jolokiaService.getFullJolokiaUrl()
return `${jolokiaUrl}/read/${escapeMBean(mbean)}/${attribute}`
}

async canInvoke(mbean: string, attribute: string, type: string): Promise<boolean> {
const aclMBean = await rbacService.getACLMBean()
if (!aclMBean) {
// Always allow invocation when client-side RBAC is not available
return true
}

const operation = 'canInvoke(java.lang.String,java.lang.String,[Ljava.lang.String;)'
const args = [mbean, `set${attribute}`, [type]]
return jolokiaService.execute(aclMBean, operation, args) as Promise<boolean>
}

async update(mbeanName: string, attribute: string, value: unknown) {
await jolokiaService.writeAttribute(mbeanName, attribute, value)
eventService.notify({ type: 'success', message: `Updated attribute: ${attribute}` })
}
}

export const attributeService = new AttributeService()

0 comments on commit 2dab258

Please sign in to comment.