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 <reet.srivastava@rackspace.com>
This commit is contained in:
@@ -37,6 +37,11 @@ export class BarbicanClient extends Base {
|
||||
key: 'containers',
|
||||
responseKey: 'container',
|
||||
},
|
||||
{
|
||||
name: 'orders',
|
||||
key: 'orders',
|
||||
responseKey: 'order',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -618,6 +618,29 @@ const renderMenu = (t) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/key-manager',
|
||||
name: t('Key Manager'),
|
||||
key: 'keyManager',
|
||||
icon: <ContainerOutlined />,
|
||||
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'),
|
||||
|
20
src/pages/barbican/App.jsx
Normal file
20
src/pages/barbican/App.jsx
Normal file
@@ -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;
|
50
src/pages/barbican/containers/Secret/Detail/BaseDetail.jsx
Normal file
50
src/pages/barbican/containers/Secret/Detail/BaseDetail.jsx
Normal file
@@ -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: <pre>{payload}</pre>,
|
||||
copyable: {
|
||||
text: payload,
|
||||
},
|
||||
},
|
||||
];
|
||||
return {
|
||||
title: t('Secret Content'),
|
||||
labelCol: 0,
|
||||
contentCol: 24,
|
||||
options,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default inject('rootStore')(observer(BaseDetail));
|
104
src/pages/barbican/containers/Secret/Detail/index.jsx
Normal file
104
src/pages/barbican/containers/Secret/Detail/index.jsx
Normal file
@@ -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 <Badge color={statusColor} text={statusText} />;
|
||||
}
|
||||
return <Badge color="#3C9E6C" text="Active" />;
|
||||
},
|
||||
},
|
||||
{
|
||||
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));
|
462
src/pages/barbican/containers/Secret/actions/Create.jsx
Normal file
462
src/pages/barbican/containers/Secret/actions/Create.jsx
Normal file
@@ -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));
|
46
src/pages/barbican/containers/Secret/actions/Delete.jsx
Normal file
46
src/pages/barbican/containers/Secret/actions/Delete.jsx
Normal file
@@ -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);
|
||||
};
|
||||
}
|
13
src/pages/barbican/containers/Secret/actions/index.js
Normal file
13
src/pages/barbican/containers/Secret/actions/index.js
Normal file
@@ -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;
|
91
src/pages/barbican/containers/Secret/index.jsx
Normal file
91
src/pages/barbican/containers/Secret/index.jsx
Normal file
@@ -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 <Badge color={statusColor} text={statusText} />;
|
||||
}
|
||||
return <Badge color="#3C9E6C" text="Active" />;
|
||||
},
|
||||
},
|
||||
{
|
||||
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));
|
35
src/pages/barbican/routes/index.jsx
Normal file
35
src/pages/barbican/routes/index.jsx
Normal file
@@ -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 },
|
||||
],
|
||||
},
|
||||
];
|
@@ -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,
|
||||
|
@@ -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'],
|
||||
|
@@ -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;
|
||||
|
56
src/stores/barbican/orders.js
Normal file
56
src/stores/barbican/orders.js
Normal file
@@ -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;
|
@@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user