From fa2959a2b6b10f885e9d1ead21be9b353df3352e Mon Sep 17 00:00:00 2001 From: Reet Srivastava Date: Tue, 26 Aug 2025 13:05:47 +0530 Subject: [PATCH] feat:adds barbican panel(named Key-Manager) This change adds barbican panel for secrets and list similar to other panels and subpanel. Has Creation , Detail Secret Page included Change-Id: I33a7c4a4845529c5a6d44060728956701f50dccf Signed-off-by: Reet Srivastava --- src/client/barbican/index.js | 5 + src/components/Form/index.jsx | 1 - src/layouts/menu.jsx | 23 + src/pages/barbican/App.jsx | 20 + .../containers/Secret/Detail/BaseDetail.jsx | 50 ++ .../containers/Secret/Detail/index.jsx | 104 ++++ .../containers/Secret/actions/Create.jsx | 462 ++++++++++++++++++ .../containers/Secret/actions/Delete.jsx | 46 ++ .../containers/Secret/actions/index.js | 13 + .../barbican/containers/Secret/index.jsx | 91 ++++ src/pages/barbican/routes/index.jsx | 35 ++ src/pages/basic/routes/index.js | 7 + src/resources/skyline/policy.js | 2 + src/stores/barbican/containers.js | 42 +- src/stores/barbican/orders.js | 56 +++ src/stores/barbican/secrets.js | 92 +++- 16 files changed, 1030 insertions(+), 19 deletions(-) create mode 100644 src/pages/barbican/App.jsx create mode 100644 src/pages/barbican/containers/Secret/Detail/BaseDetail.jsx create mode 100644 src/pages/barbican/containers/Secret/Detail/index.jsx create mode 100644 src/pages/barbican/containers/Secret/actions/Create.jsx create mode 100644 src/pages/barbican/containers/Secret/actions/Delete.jsx create mode 100644 src/pages/barbican/containers/Secret/actions/index.js create mode 100644 src/pages/barbican/containers/Secret/index.jsx create mode 100644 src/pages/barbican/routes/index.jsx create mode 100644 src/stores/barbican/orders.js diff --git a/src/client/barbican/index.js b/src/client/barbican/index.js index 7140ad60..7070f7d6 100644 --- a/src/client/barbican/index.js +++ b/src/client/barbican/index.js @@ -37,6 +37,11 @@ export class BarbicanClient extends Base { key: 'containers', responseKey: 'container', }, + { + name: 'orders', + key: 'orders', + responseKey: 'order', + }, ]; } } diff --git a/src/components/Form/index.jsx b/src/components/Form/index.jsx index 241dd368..a88411fb 100644 --- a/src/components/Form/index.jsx +++ b/src/components/Form/index.jsx @@ -315,7 +315,6 @@ export default class BaseForm extends React.Component { onOk = (values, containerProps, callback) => { // eslint-disable-next-line no-console - console.log('onOk', values); this.values = values; if (this.codeError) { return; diff --git a/src/layouts/menu.jsx b/src/layouts/menu.jsx index 9416bb72..6237d499 100644 --- a/src/layouts/menu.jsx +++ b/src/layouts/menu.jsx @@ -618,6 +618,29 @@ const renderMenu = (t) => { }, ], }, + { + path: '/key-manager', + name: t('Key Manager'), + key: 'keyManager', + icon: , + children: [ + { + path: '/key-manager/secret', + name: t('Secrets'), + key: 'keyManagerSecret', + level: 1, + children: [ + { + path: /^\/key-manager\/secret\/detail\/.[^/]+$/, + name: t('Secret Detail'), + key: 'secretDetail', + level: 2, + routePath: '/key-manager/secret/detail/:id', + }, + ], + }, + ], + }, // { // path: '/management', // name: t('Maintenance'), diff --git a/src/pages/barbican/App.jsx b/src/pages/barbican/App.jsx new file mode 100644 index 00000000..01d60365 --- /dev/null +++ b/src/pages/barbican/App.jsx @@ -0,0 +1,20 @@ +// Copyright 2025 99cloud +// +// 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. + +import renderRoutes from 'utils/RouterConfig'; +import routes from './routes'; + +const App = (props) => renderRoutes(routes, props); + +export default App; diff --git a/src/pages/barbican/containers/Secret/Detail/BaseDetail.jsx b/src/pages/barbican/containers/Secret/Detail/BaseDetail.jsx new file mode 100644 index 00000000..2930c90f --- /dev/null +++ b/src/pages/barbican/containers/Secret/Detail/BaseDetail.jsx @@ -0,0 +1,50 @@ +// Copyright 2025 99cloud +// +// 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. + +import React from 'react'; +import { inject, observer } from 'mobx-react'; +import Base from 'containers/BaseDetail'; + +export class BaseDetail extends Base { + get leftCardsStyle() { + return { + flex: 1, + }; + } + + get leftCards() { + const cards = [this.contentCard]; + return cards; + } + + get contentCard() { + const { payload } = this.props.detail; + const options = [ + { + content:
{payload}
, + copyable: { + text: payload, + }, + }, + ]; + return { + title: t('Secret Content'), + labelCol: 0, + contentCol: 24, + options, + }; + } +} + +export default inject('rootStore')(observer(BaseDetail)); diff --git a/src/pages/barbican/containers/Secret/Detail/index.jsx b/src/pages/barbican/containers/Secret/Detail/index.jsx new file mode 100644 index 00000000..fcf6578e --- /dev/null +++ b/src/pages/barbican/containers/Secret/Detail/index.jsx @@ -0,0 +1,104 @@ +// Copyright 2025 99cloud +// +// 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. + +import React from 'react'; +import { inject, observer } from 'mobx-react'; +import Base from 'containers/TabDetail'; +import { SecretsStore } from 'stores/barbican/secrets'; +import { Badge } from 'antd'; +import actionConfigs from '../actions'; +import BaseDetail from './BaseDetail'; + +export class SecretDetail extends Base { + init() { + this.store = new SecretsStore(); + } + + get policy() { + return 'secret:get'; + } + + get name() { + return t('Secret Detail'); + } + + get listUrl() { + return this.getRoutePath('keyManagerSecret'); + } + + get actionConfigs() { + return actionConfigs; + } + + get detailInfos() { + return [ + { + title: t('ID'), + dataIndex: 'id', + }, + { + title: t('Name'), + dataIndex: 'name', + }, + { + title: t('Status'), + dataIndex: 'expiration', + valueRender: 'toLocalTime', + render: (value) => { + if (value) { + const isExpired = value && new Date(value) < new Date(); + const statusText = t(isExpired ? 'Expired' : 'Active'); + const statusColor = isExpired ? '#D32F45' : '#3C9E6C'; + return ; + } + return ; + }, + }, + { + title: t('Secret Type'), + dataIndex: 'secret_type', + }, + { + title: t('Algorithm'), + dataIndex: 'algorithm', + }, + { + title: t('Mode'), + dataIndex: 'mode', + }, + { + title: t('Expiration'), + dataIndex: 'expiration', + valueRender: 'toLocalTime', + }, + { + title: t('Created At'), + dataIndex: 'created', + valueRender: 'toLocalTime', + }, + ]; + } + + get tabs() { + return [ + { + title: t('Detail Info'), + key: 'detail_info', + component: BaseDetail, + }, + ]; + } +} + +export default inject('rootStore')(observer(SecretDetail)); diff --git a/src/pages/barbican/containers/Secret/actions/Create.jsx b/src/pages/barbican/containers/Secret/actions/Create.jsx new file mode 100644 index 00000000..bc728ce2 --- /dev/null +++ b/src/pages/barbican/containers/Secret/actions/Create.jsx @@ -0,0 +1,462 @@ +// Copyright 2025 99cloud +// +// 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. + +import { inject, observer } from 'mobx-react'; +import { ModalAction } from 'containers/Action'; +import globalSecretsStore from 'stores/barbican/secrets'; +import globalOrdersStore from 'stores/barbican/orders'; +import moment from 'moment'; + +// Constants for secret configuration +const SECRET_CONFIG = { + KEY: { + BIT_LENGTH: 256, + ALGORITHM: 'aes', + }, + ASYMMETRIC: { + BIT_LENGTH: 2048, + ALGORITHM: 'rsa', + }, +}; + +export class CreateSecret extends ModalAction { + static id = 'create-secret'; + + static title = t('Create Secret'); + + static policy = ['secrets:post', 'orders:post']; + + static allowed = () => Promise.resolve(true); + + static get modalSize() { + return 'large'; + } + + static CONTENT_TYPE_OPTIONS = [ + { label: 'text/plain', value: 'text/plain' }, + { label: 'application/octet-stream', value: 'application/octet-stream' }, + { label: 'application/x-pkcs12', value: 'application/x-pkcs12' }, + { label: 'application/x-pem-file', value: 'application/x-pem-file' }, + ]; + + static ALGORITHM_OPTIONS = [ + { label: 'AES', value: 'aes' }, + { label: 'DES', value: 'des' }, + { label: '3DES', value: '3des' }, + { label: 'RSA', value: 'rsa' }, + { label: 'DSA', value: 'dsa' }, + { label: 'EC', value: 'ec' }, + ]; + + static ASYMMETRIC_ALGORITHM_OPTIONS = [ + { label: 'RSA', value: 'rsa' }, + { label: 'DSA', value: 'dsa' }, + ]; + + static BIT_LENGTH_OPTIONS = [ + { label: '128', value: 128 }, + { label: '192', value: 192 }, + { label: '256', value: 256 }, + { label: '1024', value: 1024 }, + { label: '2048', value: 2048 }, + { label: '4096', value: 4096 }, + ]; + + static MODE_OPTIONS = [ + { label: 'CBC', value: 'cbc' }, + { label: 'CTR', value: 'ctr' }, + ]; + + init() { + this.secretsStore = globalSecretsStore; + this.ordersStore = globalOrdersStore; + } + + get name() { + return t('Create Secret'); + } + + get nameForStateUpdate() { + return ['creationType', 'secret_type', 'request_type', 'algorithm']; + } + + onSecretTypeChange = (value) => { + const { creationType } = this.state; + + if (!this.formRef.current) { + return; + } + + if (creationType === 'direct') { + this.formRef.current.setFieldsValue({ + secret_type: value, + }); + return; + } + + const config = + value === 'key' + ? SECRET_CONFIG.KEY + : value === 'asymmetric' + ? SECRET_CONFIG.ASYMMETRIC + : { BIT_LENGTH: undefined, ALGORITHM: undefined }; + + this.formRef.current.setFieldsValue({ + request_type: value, + bit_length: config.BIT_LENGTH, + algorithm: config.ALGORITHM, + }); + }; + + get defaultValue() { + return { + creationType: 'direct', + name: '', + payload: '', + payload_content_type: 'text/plain', + payload_content_encoding: '', + algorithm: 'aes', + bit_length: 256, + mode: 'cbc', + secret_type: 'opaque', + request_type: 'key', + expiration: '', + }; + } + + get creationTypeOptions() { + return [ + { + label: t('Create Secret Directly'), + value: 'direct', + tip: t('Create a secret with your own payload content'), + }, + { + label: t('Create Secret via Order'), + value: 'order', + tip: t('Create a secret through order (for certificates, keys, etc.)'), + }, + ]; + } + + get requestTypeOptions() { + return [ + { + label: t('Key'), + value: 'key', + tip: t('Symmetric keys, private keys, and passphrases'), + }, + { + label: t('Asymmetric Key'), + value: 'asymmetric', + tip: t('Asymmetric key pairs (public/private)'), + }, + ]; + } + + get secretTypeOptionsForDirect() { + return [ + { + label: t('Opaque'), + value: 'opaque', + tip: t('Default secret type for arbitrary data'), + }, + { + label: t('Symmetric'), + value: 'symmetric', + tip: t('Symmetric keys'), + }, + { + label: t('Public'), + value: 'public', + tip: t('Public keys'), + }, + { + label: t('Private'), + value: 'private', + tip: t('Private keys'), + }, + { + label: t('Certificate'), + value: 'certificate', + tip: t('Certificates'), + }, + { + label: t('Passphrase'), + value: 'passphrase', + tip: t('Passphrases'), + }, + ]; + } + + get algorithmOptions() { + const { secret_type, request_type } = this.state; + const currentType = secret_type || request_type; + + if (currentType === 'asymmetric') { + return CreateSecret.ASYMMETRIC_ALGORITHM_OPTIONS; + } + + return CreateSecret.ALGORITHM_OPTIONS; + } + + get bitLengthOptions() { + return CreateSecret.BIT_LENGTH_OPTIONS; + } + + get modeOptions() { + return CreateSecret.MODE_OPTIONS; + } + + get formItems() { + const { creationType } = this.state; + const isDirect = creationType === 'direct'; + const isOrder = creationType === 'order'; + + const baseItems = this.getBaseFormItems(); + + if (isDirect) { + return [...baseItems, ...this.getDirectFormItems()]; + } + + if (isOrder) { + return [...baseItems, ...this.getOrderFormItems()]; + } + + return baseItems; + } + + getBaseFormItems() { + const { creationType } = this.state; + const isDirect = creationType === 'direct'; + const isOrder = creationType === 'order'; + + return [ + { + name: 'creationType', + label: t('Creation Method'), + type: 'radio', + options: this.creationTypeOptions, + required: true, + }, + { + name: 'name', + label: t('Secret Name'), + type: 'input-name', + required: false, + withoutChinese: true, + tip: isOrder + ? t('Optional. If not provided, a name will be auto-generated') + : t('Required for direct creation'), + rules: isDirect + ? [ + { + required: true, + message: t('Secret name is required for direct creation'), + }, + ] + : [], + }, + ]; + } + + getDirectFormItems() { + return [ + { + name: 'payload', + label: t('Secret Payload'), + type: 'textarea', + required: true, + rows: 6, + placeholder: t('Enter the secret content'), + tip: t('The actual secret data to be stored'), + }, + { + name: 'payload_content_type', + label: t('Payload Content Type'), + type: 'select', + options: CreateSecret.CONTENT_TYPE_OPTIONS, + required: true, + tip: t('Required when payload is supplied'), + }, + { + name: 'payload_content_encoding', + label: t('Payload Content Encoding'), + type: 'input', + required: false, + tip: t( + 'Required if payload content type is "application/octet-stream"' + ), + }, + { + name: 'secret_type', + label: t('Secret Type'), + type: 'select', + options: [ + { + label: t('Select Secret Type'), + value: '', + }, + ...this.secretTypeOptionsForDirect, + ], + required: false, + tip: t('Optional. Type of secret being stored (default: opaque)'), + }, + { + name: 'algorithm', + label: t('Algorithm'), + type: 'select', + options: [ + { + label: t('Select Algorithm'), + value: '', + }, + ...this.algorithmOptions, + ], + required: false, + tip: t('Optional. Algorithm used by the secret (default: aes)'), + }, + { + name: 'bit_length', + label: t('Bit Length'), + type: 'select', + options: [ + { + label: t('Select Bit Length'), + value: '', + }, + ...this.bitLengthOptions, + ], + required: false, + tip: t('Optional. Bit length of the secret (default: 256)'), + }, + { + name: 'mode', + label: t('Mode'), + type: 'select', + options: [ + { + label: t('Select Mode'), + value: '', + }, + ...this.modeOptions, + ], + required: false, + tip: t( + 'Optional. Algorithm mode, used only for reference (default: cbc)' + ), + }, + { + name: 'expiration', + label: t('Expiration Date'), + type: 'date-picker', + showToday: false, + disabledDate: (current) => current && current <= moment().endOf('d'), + tip: t('Optional. When the secret should expire (ISO 8601 format)'), + }, + ]; + } + + getOrderFormItems() { + return [ + { + name: 'request_type', + label: t('Request Type'), + type: 'select', + options: this.requestTypeOptions, + required: true, + onChange: this.onSecretTypeChange, + }, + { + name: 'algorithm', + label: t('Algorithm'), + type: 'select', + options: this.algorithmOptions, + required: false, + tip: t( + 'Optional. Algorithm to be used with the requested key (default: aes)' + ), + }, + { + name: 'bit_length', + label: t('Bit Length'), + type: 'select', + options: this.bitLengthOptions, + required: false, + tip: t( + 'Optional. Bit length of the requested secret key (default: 256)' + ), + }, + { + name: 'mode', + label: t('Mode'), + type: 'select', + options: this.modeOptions, + required: false, + tip: t( + 'Optional. Algorithm mode to be used with the requested key (default: cbc)' + ), + }, + { + name: 'payload_content_type', + label: t('Payload Content Type'), + type: 'select', + options: [ + { + label: 'application/octet-stream', + value: 'application/octet-stream', + }, + { label: 'text/plain', value: 'text/plain' }, + { label: 'application/x-pkcs12', value: 'application/x-pkcs12' }, + { + label: 'application/x-pem-file', + value: 'application/x-pem-file', + }, + ], + required: false, + tip: t( + 'Optional. Type/format of the secret to be generated (default: application/octet-stream)' + ), + }, + { + name: 'expiration', + label: t('Expiration Date'), + type: 'date-picker', + showToday: false, + disabledDate: (current) => current && current <= moment().endOf('d'), + tip: t('Optional. Expiration time for the secret in ISO 8601 format'), + }, + ]; + } + + onClickCancel = () => { + this.onCancel(); + }; + + onSubmit = (values) => { + const { creationType, ...rest } = values; + + if (creationType === 'direct') { + return this.secretsStore.create(rest); + } + + if (creationType === 'order') { + return this.ordersStore.create(rest); + } + + return Promise.reject(new Error('Invalid creation type')); + }; +} + +export default inject('rootStore')(observer(CreateSecret)); diff --git a/src/pages/barbican/containers/Secret/actions/Delete.jsx b/src/pages/barbican/containers/Secret/actions/Delete.jsx new file mode 100644 index 00000000..9dcd798d --- /dev/null +++ b/src/pages/barbican/containers/Secret/actions/Delete.jsx @@ -0,0 +1,46 @@ +// Copyright 2025 99cloud +// +// 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. + +import { ConfirmAction } from 'containers/Action'; +import globalSecretsStore from 'stores/barbican/secrets'; + +export default class DeleteAction extends ConfirmAction { + static policy = 'secret:delete'; + + static allowed = () => Promise.resolve(true); + + get id() { + return 'delete'; + } + + get title() { + return t('Delete Secret'); + } + + get isDanger() { + return true; + } + + get buttonText() { + return t('Delete'); + } + + get actionName() { + return t('delete secret'); + } + + onSubmit = (data) => { + return globalSecretsStore.delete(data); + }; +} diff --git a/src/pages/barbican/containers/Secret/actions/index.js b/src/pages/barbican/containers/Secret/actions/index.js new file mode 100644 index 00000000..8f4eb392 --- /dev/null +++ b/src/pages/barbican/containers/Secret/actions/index.js @@ -0,0 +1,13 @@ +import CreateAction from './Create'; +import DeleteAction from './Delete'; + +const actionConfigs = { + rowActions: { + firstAction: DeleteAction, + moreActions: [], + }, + batchActions: [DeleteAction], + primaryActions: [CreateAction], +}; + +export default actionConfigs; diff --git a/src/pages/barbican/containers/Secret/index.jsx b/src/pages/barbican/containers/Secret/index.jsx new file mode 100644 index 00000000..48aff5fe --- /dev/null +++ b/src/pages/barbican/containers/Secret/index.jsx @@ -0,0 +1,91 @@ +// Copyright 2025 99cloud +// +// 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. + +import React from 'react'; +import { observer, inject } from 'mobx-react'; +import BaseList from 'containers/List'; +import globalSecretsStore from 'stores/barbican/secrets'; +import { Badge } from 'antd'; +import actionConfigs from './actions'; + +export class SecretList extends BaseList { + init() { + this.store = globalSecretsStore; + } + + get name() { + return t('Secrets'); + } + + get policy() { + return 'barbican:secret:get'; + } + + getColumns() { + return [ + { + title: t('ID/Name'), + dataIndex: 'name', + routeName: this.getRouteName('secretDetail'), + }, + { + title: t('Status'), + dataIndex: 'expiration', + valueRender: 'toLocalTime', + render: (value) => { + if (value) { + const isExpired = value && new Date(value) < new Date(); + const statusText = t(isExpired ? 'Expired' : 'Active'); + const statusColor = isExpired ? '#D32F45' : '#3C9E6C'; + return ; + } + return ; + }, + }, + { + title: t('Algorithm'), + dataIndex: 'algorithm', + }, + { + title: t('Expiration'), + dataIndex: 'expiration', + valueRender: 'toLocalTime', + }, + { + title: t('Created At'), + dataIndex: 'created', + valueRender: 'toLocalTime', + }, + ]; + } + + get actionConfigs() { + return actionConfigs; + } + + get searchFilters() { + return [ + { + label: t('Name'), + name: 'name', + }, + { + label: t('Algorithm'), + name: 'algorithm', + }, + ]; + } +} + +export default inject('rootStore')(observer(SecretList)); diff --git a/src/pages/barbican/routes/index.jsx b/src/pages/barbican/routes/index.jsx new file mode 100644 index 00000000..2c8b5c27 --- /dev/null +++ b/src/pages/barbican/routes/index.jsx @@ -0,0 +1,35 @@ +// Copyright 2025 99cloud +// +// 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. + +import BaseLayout from 'layouts/Basic'; +import E404 from 'pages/base/containers/404'; +import SecretList from '../containers/Secret'; +import SecretDetail from '../containers/Secret/Detail'; + +const PATH = '/key-manager'; +export default [ + { + path: PATH, + component: BaseLayout, + routes: [ + { path: `${PATH}/secret`, component: SecretList, exact: true }, + { + path: `${PATH}/secret/detail/:id`, + component: SecretDetail, + exact: true, + }, + { path: '*', component: E404 }, + ], + }, +]; diff --git a/src/pages/basic/routes/index.js b/src/pages/basic/routes/index.js index 556d0c22..08362cd7 100644 --- a/src/pages/basic/routes/index.js +++ b/src/pages/basic/routes/index.js @@ -50,6 +50,9 @@ const Database = lazy(() => const Share = lazy(() => import(/* webpackChunkName: "share" */ 'pages/share/App') ); +const Barbican = lazy(() => + import(/* webpackChunkName: "barbican" */ 'pages/barbican/App') +); const ContainerInfra = lazy(() => import(/* webpackChunkName: "container-infra" */ 'pages/container-infra/App') ); @@ -111,6 +114,10 @@ export default [ path: `/share`, component: Share, }, + { + path: `/key-manager`, + component: Barbican, + }, { path: `/container-infra`, component: ContainerInfra, diff --git a/src/resources/skyline/policy.js b/src/resources/skyline/policy.js index 250f7deb..d332a189 100644 --- a/src/resources/skyline/policy.js +++ b/src/resources/skyline/policy.js @@ -71,6 +71,8 @@ export const policyMap = { 'secret:decrypt', 'secret:delete', 'containers:post', + 'secrets:post', + 'orders:post', ], zun: ['capsule:', 'container:', 'host:get'], panko: ['segregation', 'telemetry:events:index'], diff --git a/src/stores/barbican/containers.js b/src/stores/barbican/containers.js index b7350014..dfcb9bed 100644 --- a/src/stores/barbican/containers.js +++ b/src/stores/barbican/containers.js @@ -150,11 +150,45 @@ export class ContainersStore extends Base { }); // Determine if the certificate is used in the listener this.updateItem(item, listeners); - // Fetch secrets payload + // Fetch secrets payload with proper decoding const payloads = await Promise.all( - secretIds.map((id) => - this.payloadClient.list(id, {}, { headers: { Accept: 'text/plain' } }) - ) + secretIds.map(async (id) => { + try { + const payload = await this.payloadClient.list(id, null, { + headers: { + Accept: '*/*', + }, + responseType: 'arraybuffer', + }); + + // Decode payload if it's binary + if (payload instanceof ArrayBuffer) { + const bytes = new Uint8Array(payload); + + // Check if it's actually text by trying to decode it + const textDecoder = new TextDecoder('utf-8', { fatal: false }); + const decodedText = textDecoder.decode(bytes); + + // Check if the decoded text contains valid printable characters + const isValidText = /^[\x20-\x7E\n\r\t]*$/.test(decodedText); + + if (isValidText) { + // It's valid text, use it + return decodedText; + } + // It's binary data, convert to base64 + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); + } + return payload; + } catch (error) { + console.error(`Failed to fetch payload for secret ${id}:`, error); + return null; + } + }) ); (payloads || []).forEach((it, index) => { secret_refs[index].secret_info.payload = it; diff --git a/src/stores/barbican/orders.js b/src/stores/barbican/orders.js new file mode 100644 index 00000000..cda5769e --- /dev/null +++ b/src/stores/barbican/orders.js @@ -0,0 +1,56 @@ +// Copyright 2025 99cloud +// +// 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. + +import Base from 'stores/base'; +import client from 'client'; +import { action } from 'mobx'; + +export class OrdersStore extends Base { + get client() { + return client.barbican.orders; + } + + get responseKey() { + return 'order'; + } + + @action + async create(data) { + const { + expiration, + request_type, + algorithm, + bit_length, + name, + payload_content_type, + mode, + } = data; + + const body = { + type: request_type, + meta: { + ...(algorithm && { algorithm }), + ...(bit_length && { bit_length }), + ...(mode && { mode }), + ...(payload_content_type && { payload_content_type }), + ...(name && { name }), + ...(expiration && { expiration }), + }, + }; + return this.client.create(body); + } +} + +const globalOrdersStore = new OrdersStore(); +export default globalOrdersStore; diff --git a/src/stores/barbican/secrets.js b/src/stores/barbican/secrets.js index dad52682..b4e0b7a8 100644 --- a/src/stores/barbican/secrets.js +++ b/src/stores/barbican/secrets.js @@ -43,14 +43,21 @@ export class SecretsStore extends Base { get mapper() { return (data) => { - const { secret_ref, algorithm } = data; + const { secret_ref, algorithm, expiration } = data; const [, uuid] = secret_ref.split('/secrets/'); - const { domain, expiration } = algorithm ? JSON.parse(algorithm) : {}; + let extractedExpiration = expiration; + if (algorithm && algorithm.startsWith('{')) { + try { + const parsed = JSON.parse(algorithm); + extractedExpiration = parsed.expiration || expiration; + } catch { + // Do nothing, Keep original expiration if parsing fails + } + } return { ...data, id: uuid, - domain, - expiration, + expiration: extractedExpiration, }; }; } @@ -97,11 +104,58 @@ export class SecretsStore extends Base { this.isLoading = true; } const [item, payload, listeners] = await Promise.all([ - this.client.show(id, {}, { headers: { Accept: 'application/json' } }), - this.payloadClient.list(id, {}, { headers: { Accept: 'text/plain' } }), + this.client.show(id, null, { + headers: { + Accept: 'application/json', + }, + }), + this.payloadClient.list(id, null, { + headers: { + Accept: '*/*', + }, + responseType: 'arraybuffer', + }), globalListenerStore.fetchList(), ]); - item.payload = payload; + + let decodedPayload = payload; + if (payload) { + try { + if (payload instanceof ArrayBuffer) { + const bytes = new Uint8Array(payload); + const contentType = item.payload_content_type || 'text/plain'; + + const textDecoder = new TextDecoder('utf-8', { fatal: false }); + const decodedText = textDecoder.decode(bytes); + + const isValidText = /^[\x20-\x7E\n\r\t]*$/.test(decodedText); + + if ( + isValidText && + (contentType.includes('text') || + contentType.includes('plain') || + contentType.includes('json')) + ) { + decodedPayload = decodedText; + } else { + let binary = ''; + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + decodedPayload = btoa(binary); + } + } else if (typeof payload === 'string') { + decodedPayload = payload; + } else { + decodedPayload = JSON.stringify(payload, null, 2); + } + } catch (error) { + console.error('Error decoding payload:', error); + decodedPayload = 'Error decoding payload'; + } + } + + item.payload = decodedPayload; // Determine if the certificate is used in the listener this.updateItem(item, listeners); const detail = this.mapper(item || {}); @@ -124,15 +178,25 @@ export class SecretsStore extends Base { @action async create(data) { - const { expiration, domain, algorithm, ...rest } = data; + const { + expiration, + secret_type, + algorithm, + bit_length, + mode, + payload_content_encoding, + ...rest + } = data; + const body = { ...rest, - algorithm: - algorithm || - JSON.stringify({ - domain, - expiration, - }), + ...(secret_type && secret_type.trim() !== '' && { secret_type }), + ...(algorithm && algorithm.trim() !== '' && { algorithm }), + ...(bit_length && { bit_length }), + ...(mode && mode.trim() !== '' && { mode }), + ...(payload_content_encoding && + payload_content_encoding.trim() !== '' && { payload_content_encoding }), + ...(expiration && { expiration }), }; return this.client.create(body); }