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);
}