mirror of
https://github.com/certimate-go/certimate.git
synced 2026-10-16 15:16:05 +08:00
feat(provider): new purge provider: azure keyvault
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package purgers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/certimate-go/certimate/internal/domain"
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
pgrimpl "github.com/certimate-go/certimate/pkg/core/purger/providers/azure-keyvault"
|
||||
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Registries.MustRegister(domain.PurgeProviderTypeAzureKeyVault, func(options *ProviderFactoryOptions) (core.Purger, error) {
|
||||
credentials := domain.AccessConfigForAzure{}
|
||||
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
|
||||
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
|
||||
}
|
||||
|
||||
provider, err := pgrimpl.NewPurger(&pgrimpl.PurgerConfig{
|
||||
TenantId: credentials.TenantId,
|
||||
ClientId: credentials.ClientId,
|
||||
ClientSecret: credentials.ClientSecret,
|
||||
CloudName: credentials.CloudName,
|
||||
KeyVaultName: xmaps.GetString(options.ProviderExtendedConfig, "keyvaultName"),
|
||||
})
|
||||
return provider, err
|
||||
})
|
||||
}
|
||||
@@ -495,6 +495,7 @@ const (
|
||||
PurgeProviderTypeAliyunCAS = PurgeProviderType(AccessProviderTypeAliyun + "-cas")
|
||||
PurgeProviderTypeAliyunCLB = PurgeProviderType(AccessProviderTypeAliyun + "-clb")
|
||||
PurgeProviderTypeAliyunESA = PurgeProviderType(AccessProviderTypeAliyun + "-esa")
|
||||
PurgeProviderTypeAzureKeyVault = PurgeProviderType(AccessProviderTypeAzure + "-keyvault")
|
||||
PurgeProviderTypeBaiduCloudCert = PurgeProviderType(AccessProviderTypeBaiduCloud + "-cert")
|
||||
PurgeProviderTypeHuaweiCloudELB = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-elb")
|
||||
PurgeProviderTypeHuaweiCloudSCM = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-scm")
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//go:build tester
|
||||
|
||||
package azurekeyvault_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
tester "github.com/certimate-go/certimate/pkg/core/purger/providers-tester"
|
||||
impl "github.com/certimate-go/certimate/pkg/core/purger/providers/azure-keyvault"
|
||||
)
|
||||
|
||||
var (
|
||||
fp = tester.InitArgs("AZUREKEYVAULT_")
|
||||
fTenantId string
|
||||
fClientId string
|
||||
fClientSecret string
|
||||
fCloudName string
|
||||
fKeyVaultName string
|
||||
)
|
||||
|
||||
func init() {
|
||||
fp.DefineString(&fTenantId, "TENANTID")
|
||||
fp.DefineString(&fClientId, "CLIENTID")
|
||||
fp.DefineString(&fClientSecret, "CLIENTSECRET")
|
||||
fp.DefineString(&fCloudName, "CLOUDNAME")
|
||||
fp.DefineString(&fKeyVaultName, "KEYVAULTNAME")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -tags=tester -v ./azure_keyvault_test.go -args \
|
||||
--AZUREKEYVAULT_TENANTID="your-tenant-id" \
|
||||
--AZUREKEYVAULT_CLIENTID="your-app-registration-client-id" \
|
||||
--AZUREKEYVAULT_CLIENTSECRET="your-app-registration-client-secret" \
|
||||
--AZUREKEYVAULT_CLOUDNAME="china" \
|
||||
--AZUREKEYVAULT_KEYVAULTNAME="your-keyvault-name"
|
||||
*/
|
||||
func TestProvider(t *testing.T) {
|
||||
fp.Parse()
|
||||
|
||||
t.Run("Purge", func(t *testing.T) {
|
||||
provider, err := impl.NewPurger(&impl.PurgerConfig{
|
||||
TenantId: fTenantId,
|
||||
ClientId: fClientId,
|
||||
ClientSecret: fClientSecret,
|
||||
CloudName: fCloudName,
|
||||
KeyVaultName: fKeyVaultName,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tester.Purge(t, provider, tester.PurgeInput{})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package azurekeyvault
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/certimate-go/certimate/pkg/core"
|
||||
xloop "github.com/certimate-go/certimate/pkg/utils/loop"
|
||||
xazure "github.com/certimate-go/certimate/pkg/utils/third-party/azure"
|
||||
xwait "github.com/certimate-go/certimate/pkg/utils/wait"
|
||||
)
|
||||
|
||||
type (
|
||||
Provider = core.Purger
|
||||
PurgeResult = core.PurgerPurgeResult
|
||||
)
|
||||
|
||||
type PurgerConfig struct {
|
||||
// Azure TenantId。
|
||||
TenantId string `json:"tenantId"`
|
||||
// Azure ClientId。
|
||||
ClientId string `json:"clientId"`
|
||||
// Azure ClientSecret。
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
// Azure 主权云环境。
|
||||
CloudName string `json:"cloudName,omitempty"`
|
||||
// Key Vault 名称。
|
||||
KeyVaultName string `json:"keyvaultName"`
|
||||
}
|
||||
|
||||
type Purger struct {
|
||||
config *PurgerConfig
|
||||
logger *slog.Logger
|
||||
sdkClient *azcertificates.Client
|
||||
}
|
||||
|
||||
var _ Provider = (*Purger)(nil)
|
||||
|
||||
func NewPurger(config *PurgerConfig) (*Purger, error) {
|
||||
if config == nil {
|
||||
return nil, fmt.Errorf("the configuration of the purger provider is nil")
|
||||
}
|
||||
|
||||
client, err := createSDKClient(config.CloudName, config.TenantId, config.ClientId, config.ClientSecret, config.KeyVaultName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create client: %w", err)
|
||||
}
|
||||
|
||||
return &Purger{
|
||||
config: config,
|
||||
logger: slog.Default(),
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Purger) SetLogger(logger *slog.Logger) {
|
||||
if logger == nil {
|
||||
p.logger = slog.New(slog.DiscardHandler)
|
||||
} else {
|
||||
p.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Purger) Purge(ctx context.Context, expiry time.Duration) (*PurgeResult, error) {
|
||||
purgingCertIds := make([]azcertificates.ID, 0)
|
||||
|
||||
// 获取证书列表
|
||||
// REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/get-certificates/get-certificates
|
||||
listCertificatesPager := p.sdkClient.NewListCertificatePropertiesPager(&azcertificates.ListCertificatePropertiesOptions{})
|
||||
for listCertificatesPager.More() {
|
||||
page, err := listCertificatesPager.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute sdk request 'keyvault.GetCertificates': %w", err)
|
||||
}
|
||||
|
||||
for _, certItem := range page.Value {
|
||||
if certItem.Attributes == nil || certItem.Attributes.Expires == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
certNotAfter := lo.FromPtr(certItem.Attributes.Expires)
|
||||
if time.Since(certNotAfter) >= expiry {
|
||||
purgingCertIds = append(purgingCertIds, *certItem.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除证书
|
||||
// REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/delete-certificate/delete-certificate
|
||||
purgedCount := 0
|
||||
if len(purgingCertIds) > 0 {
|
||||
p.logger.Info("found certificates to purge", slog.Any("certIds", purgingCertIds))
|
||||
|
||||
if err := xloop.ForRangeAllWithContext(ctx, purgingCertIds, func(ctx context.Context, certId azcertificates.ID, i int) error {
|
||||
if i > 0 {
|
||||
if err := xwait.DelayWithContext(ctx, 1*time.Second); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
deleteCertificateResp, err := p.sdkClient.DeleteCertificate(ctx, certId.Name(), nil)
|
||||
p.logger.Debug("sdk request 'keyvault.DeleteCertificate'", slog.String("params.certificateName", certId.Name()), slog.Any("response", deleteCertificateResp))
|
||||
if err != nil {
|
||||
var sdkErr *azcore.ResponseError
|
||||
if errors.As(err, &sdkErr) {
|
||||
if sdkErrCode := sdkErr.ErrorCode; sdkErrCode == "ResourceNotFound" || sdkErrCode == "CertificateNotFound" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to execute sdk request 'keyvault.DeleteCertificate': %w", err)
|
||||
}
|
||||
|
||||
purgedCount++
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &PurgeResult{
|
||||
Count: purgedCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func createSDKClient(cloudName, tenantId, clientId, clientSecret, keyvaultName string) (*azcertificates.Client, error) {
|
||||
env, err := xazure.GetCloudEnvConfiguration(cloudName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientOptions := azcore.ClientOptions{Cloud: env}
|
||||
credential, err := azidentity.NewClientSecretCredential(tenantId, clientId, clientSecret,
|
||||
&azidentity.ClientSecretCredentialOptions{ClientOptions: clientOptions})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("https://%s.vault.azure.net", keyvaultName)
|
||||
if xazure.IsUSGovernmentEnv(cloudName) {
|
||||
endpoint = fmt.Sprintf("https://%s.vault.usgovcloudapi.net", keyvaultName)
|
||||
} else if xazure.IsChinaEnv(cloudName) {
|
||||
endpoint = fmt.Sprintf("https://%s.vault.azure.cn", keyvaultName)
|
||||
}
|
||||
|
||||
client, err := azcertificates.NewClient(endpoint, credential, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { PURGE_PROVIDERS, type PurgeProviderType } from "@/domain/provider";
|
||||
import BizPurgeNodeConfigFieldsProviderAliyunCAS from "./BizPurgeNodeConfigFieldsProviderAliyunCAS";
|
||||
import BizPurgeNodeConfigFieldsProviderAliyunCLB from "./BizPurgeNodeConfigFieldsProviderAliyunCLB";
|
||||
import BizPurgeNodeConfigFieldsProviderAliyunESA from "./BizPurgeNodeConfigFieldsProviderAliyunESA";
|
||||
import BizPurgeNodeConfigFieldsProviderAzureKeyVault from "./BizPurgeNodeConfigFieldsProviderAzureKeyVault";
|
||||
import BizPurgeNodeConfigFieldsProviderHuaweiCloudELB from "./BizPurgeNodeConfigFieldsProviderHuaweiCloudELB";
|
||||
import BizPurgeNodeConfigFieldsProviderHuaweiCloudWAF from "./BizPurgeNodeConfigFieldsProviderHuaweiCloudWAF";
|
||||
import BizPurgeNodeConfigFieldsProviderTencentCloudGAAP from "./BizPurgeNodeConfigFieldsProviderTencentCloudGAAP";
|
||||
@@ -21,6 +22,7 @@ const providerComponentMap: Partial<Record<PurgeProviderType, React.ComponentTyp
|
||||
[PURGE_PROVIDERS.ALIYUN_CAS]: BizPurgeNodeConfigFieldsProviderAliyunCAS,
|
||||
[PURGE_PROVIDERS.ALIYUN_CLB]: BizPurgeNodeConfigFieldsProviderAliyunCLB,
|
||||
[PURGE_PROVIDERS.ALIYUN_ESA]: BizPurgeNodeConfigFieldsProviderAliyunESA,
|
||||
[PURGE_PROVIDERS.AZURE_KEYVAULT]: BizPurgeNodeConfigFieldsProviderAzureKeyVault,
|
||||
[PURGE_PROVIDERS.HUAWEICLOUD_ELB]: BizPurgeNodeConfigFieldsProviderHuaweiCloudELB,
|
||||
[PURGE_PROVIDERS.HUAWEICLOUD_WAF]: BizPurgeNodeConfigFieldsProviderHuaweiCloudWAF,
|
||||
[PURGE_PROVIDERS.TENCENTCLOUD_GAAP]: BizPurgeNodeConfigFieldsProviderTencentCloudGAAP,
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { getI18n, useTranslation } from "react-i18next";
|
||||
import { Form, Input } from "antd";
|
||||
import { createSchemaFieldRule } from "antd-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useFormNestedFieldsContext } from "./_context";
|
||||
|
||||
const BizPurgeNodeConfigFieldsProviderAzureKeyVault = () => {
|
||||
const { i18n, t } = useTranslation();
|
||||
|
||||
const { parentNamePath } = useFormNestedFieldsContext();
|
||||
const formSchema = z.object({
|
||||
[parentNamePath]: getSchema({ i18n }),
|
||||
});
|
||||
const formRule = createSchemaFieldRule(formSchema);
|
||||
const initialValues = getInitialValues();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name={[parentNamePath, "keyvaultName"]}
|
||||
initialValue={initialValues.keyvaultName}
|
||||
label={t("workflow_node.purge.form.azure_keyvault_name.label")}
|
||||
rules={[formRule]}
|
||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.purge.form.azure_keyvault_name.tooltip") }}></span>}
|
||||
>
|
||||
<Input placeholder={t("workflow_node.purge.form.azure_keyvault_name.placeholder")} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
|
||||
return {
|
||||
keyvaultName: "",
|
||||
};
|
||||
};
|
||||
|
||||
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
|
||||
const { t: _ } = i18n;
|
||||
|
||||
return z.object({
|
||||
keyvaultName: z.string().nonempty(),
|
||||
});
|
||||
};
|
||||
|
||||
const _default = Object.assign(BizPurgeNodeConfigFieldsProviderAzureKeyVault, {
|
||||
getInitialValues,
|
||||
getSchema,
|
||||
});
|
||||
|
||||
export default _default;
|
||||
@@ -989,6 +989,7 @@ export const PURGE_PROVIDERS = Object.freeze(
|
||||
ALIYUN_CAS: `${ACCESS_PROVIDERS.ALIYUN}-cas`,
|
||||
ALIYUN_CLB: `${ACCESS_PROVIDERS.ALIYUN}-clb`,
|
||||
ALIYUN_ESA: `${ACCESS_PROVIDERS.ALIYUN}-esa`,
|
||||
AZURE_KEYVAULT: `${ACCESS_PROVIDERS.AZURE}-keyvault`,
|
||||
BAIDUCLOUD_CERT: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cert`,
|
||||
HUAWEICLOUD_ELB: `${ACCESS_PROVIDERS.HUAWEICLOUD}-elb`,
|
||||
HUAWEICLOUD_SCM: `${ACCESS_PROVIDERS.HUAWEICLOUD}-scm`,
|
||||
@@ -1023,6 +1024,7 @@ export const purgeProvidersMap: Map<PurgeProvider["type"] | string, PurgeProvide
|
||||
[PURGE_PROVIDERS.HUAWEICLOUD_ELB, "provider.huaweicloud_elb"],
|
||||
[PURGE_PROVIDERS.HUAWEICLOUD_WAF, "provider.huaweicloud_waf"],
|
||||
[PURGE_PROVIDERS.VOLCENGINE_CERTCENTER, "provider.volcengine_certcenter"],
|
||||
[PURGE_PROVIDERS.AZURE_KEYVAULT, "provider.azure_keyvault"],
|
||||
[PURGE_PROVIDERS.BAIDUCLOUD_CERT, "provider.baiducloud_cert"],
|
||||
[PURGE_PROVIDERS.JDCLOUD_SSL, "provider.jdcloud_ssl"],
|
||||
[PURGE_PROVIDERS.UCLOUD_USSL, "provider.ucloud_ussl"],
|
||||
|
||||
@@ -3052,6 +3052,11 @@
|
||||
"placeholder": "Please enter Alibaba Cloud ESA site ID",
|
||||
"tooltip": "For more information, see <a href=\"https://esa.console.aliyun.com/siteManage/list\" target=\"_blank\">https://esa.console.aliyun.com/siteManage/list</a>"
|
||||
},
|
||||
"azure_keyvault_name": {
|
||||
"label": "Azure KeyVault name",
|
||||
"placeholder": "Please enter Azure KeyVault name",
|
||||
"tooltip": "For more information, see <a href=\"https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates\" target=\"_blank\">https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates</a>"
|
||||
},
|
||||
"huaweicloud_elb_region": {
|
||||
"label": "Huawei Cloud region",
|
||||
"placeholder": "Please enter Huawei Cloud ELB region (e.g. cn-north-1)",
|
||||
|
||||
@@ -3051,6 +3051,11 @@
|
||||
"placeholder": "请输入阿里云 ESA 站点 ID",
|
||||
"tooltip": "这是什么?请参阅 <a href=\"https://esa.console.aliyun.com/siteManage/list\" target=\"_blank\">https://esa.console.aliyun.com/siteManage/list</a>"
|
||||
},
|
||||
"azure_keyvault_name": {
|
||||
"label": "Azure KeyVault 名称",
|
||||
"placeholder": "请输入 Azure KeyVault 名称",
|
||||
"tooltip": "这是什么?请参阅 <a href=\"https://learn.microsoft.com/zh-cn/azure/key-vault/general/about-keys-secrets-certificates\" target=\"_blank\">https://learn.microsoft.com/zh-cn/azure/key-vault/general/about-keys-secrets-certificates</a>"
|
||||
},
|
||||
"huaweicloud_elb_region": {
|
||||
"label": "华为云服务区域",
|
||||
"placeholder": "请输入华为云 ELB 服务区域(例如:cn-north-1)",
|
||||
|
||||
Reference in New Issue
Block a user