diff --git a/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go b/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go index 1231ec1d5..be4edda8b 100644 --- a/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go +++ b/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go @@ -125,16 +125,18 @@ func (d *Deployer) deployToWAF3(ctx context.Context, certPEM, privkeyPEM string) d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } + certId, ok := upres.ExtendedData["CertIdentifier"].(string) + if !ok || certId == "" { + return fmt.Errorf("invalid certificate identifier in upload response") + } // 根据接入方式决定部署方式 switch d.config.ServiceType { case SERVICE_TYPE_CLOUDRESOURCE: - certId := upres.ExtendedData["CertIdentifier"].(string) if err := d.deployToWAF3WithCloudResource(ctx, certId); err != nil { return err } case SERVICE_TYPE_CNAME: - certId := upres.ExtendedData["CertIdentifier"].(string) if err := d.deployToWAF3WithCNAME(ctx, certId); err != nil { return err } @@ -147,20 +149,23 @@ func (d *Deployer) deployToWAF3(ctx context.Context, certPEM, privkeyPEM string) } func (d *Deployer) deployToWAF3WithCloudResource(ctx context.Context, cloudCertId string) error { + // 参数校验 if d.config.ResourceProduct == "" { return errors.New("config `resourceProduct` is required") } if d.config.ResourceId == "" { return errors.New("config `resourceId` is required") } + // 默认端口为 443 if d.config.ResourcePort == 0 { d.config.ResourcePort = 443 } - // 查询已同步的云产品资产 + // DescribeProductInstances + // 查询云产品实例列表,包含实例的基本信息和接入端口等信息。 // REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describeproductinstances var resourceInstance *aliwaf.DescribeProductInstancesResponseBodyProductInstances - var resourceInstancePort *aliwaf.DescribeProductInstancesResponseBodyProductInstancesResourcePorts + var resourceInstancePort *aliwaf.DescribeProductInstancesResponseBodyProductInstancesAccessPortAndProtocols describeProductInstancesReq := &aliwaf.DescribeProductInstancesRequest{ ResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), RegionId: tea.String(d.config.Region), @@ -177,7 +182,7 @@ func (d *Deployer) deployToWAF3WithCloudResource(ctx context.Context, cloudCertI } else { resourceInstance = describeProductInstancesResp.Body.ProductInstances[0] - resourceInstancePort, _ = lo.Find(resourceInstance.ResourcePorts, func(p *aliwaf.DescribeProductInstancesResponseBodyProductInstancesResourcePorts) bool { + resourceInstancePort, _ = lo.Find(resourceInstance.AccessPortAndProtocols, func(p *aliwaf.DescribeProductInstancesResponseBodyProductInstancesAccessPortAndProtocols) bool { return tea.Int32Value(p.Port) == d.config.ResourcePort }) if resourceInstancePort == nil { @@ -185,89 +190,148 @@ func (d *Deployer) deployToWAF3WithCloudResource(ctx context.Context, cloudCertI } } - // 查询云产品实例的证书列表 - var resourceInstanceCertificates []*aliwaf.DescribeResourceInstanceCertsResponseBodyCerts = make([]*aliwaf.DescribeResourceInstanceCertsResponseBodyCerts, 0) - describeResourceInstanceCertsPageNumber := 1 - describeResourceInstanceCertsPageSize := 10 - for { - select { - case <-ctx.Done(): - return ctx.Err() - default: - } + // DescribeResourceInstanceCerts + // 查询云产品实例已同步的证书列表。 + // 这个接口返回的证书列表里包含了所有已同步的证书信息,包括证书 ID、证书公共名称、过期时间等信息。 + // 但是不会区分哪些是默认证书,哪些是扩展证书。 + // 而且此接口可能会返回当前 WAF 下所有的证书,而不只是指定的的云实例使用的证书。 + // REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describeresourceinstancecerts + var resourceInstanceCertificates = make([]*aliwaf.DescribeResourceInstanceCertsResponseBodyCerts, 0) - describeResourceInstanceCertsReq := &aliwaf.DescribeResourceInstanceCertsRequest{ - ResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), - InstanceId: tea.String(d.config.InstanceId), - ResourceInstanceId: tea.String(d.config.ResourceId), - PageNumber: tea.Int64(int64(describeResourceInstanceCertsPageNumber)), - PageSize: tea.Int64(int64(describeResourceInstanceCertsPageSize)), - } - describeResourceInstanceCertsResp, err := d.sdkClient.DescribeResourceInstanceCertsWithContext(ctx, describeResourceInstanceCertsReq, &dara.RuntimeOptions{}) - d.logger.Debug("sdk request 'waf.DescribeResourceInstanceCerts'", slog.Any("request", describeResourceInstanceCertsReq), slog.Any("response", describeResourceInstanceCertsResp)) - if err != nil { - return fmt.Errorf("failed to execute sdk request 'wafw.DescribeResourceInstanceCerts': %w", err) - } - - if describeResourceInstanceCertsResp.Body == nil { - break - } - - resourceInstanceCertificates = append(resourceInstanceCertificates, describeResourceInstanceCertsResp.Body.Certs...) - - if len(describeResourceInstanceCertsResp.Body.Certs) < describeResourceInstanceCertsPageSize { - break - } - - describeResourceInstanceCertsPageNumber++ - } - - // 生成请求参数 - modifyCloudResourceReq := &aliwaf.ModifyCloudResourceRequest{ - ResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), + describeResourceInstanceCertsReq := &aliwaf.DescribeResourceInstanceCertsRequest{ RegionId: tea.String(d.config.Region), - Listen: &aliwaf.ModifyCloudResourceRequestListen{ - ResourceProduct: resourceInstance.ResourceProduct, - ResourceInstanceId: resourceInstance.ResourceInstanceId, - Protocol: tea.String("https"), - Port: resourceInstancePort.Port, - Certificates: lo.Map(resourceInstancePort.Certificates, func(c *aliwaf.DescribeProductInstancesResponseBodyProductInstancesResourcePortsCertificates, _ int) *aliwaf.ModifyCloudResourceRequestListenCertificates { - return &aliwaf.ModifyCloudResourceRequestListenCertificates{ - CertificateId: c.CertificateId, - AppliedType: c.AppliedType, - } - }), - }, + ResourceManagerResourceGroupId: lo.EmptyableToPtr(d.config.ResourceGroupId), + InstanceId: tea.String(d.config.InstanceId), + ResourceInstanceId: tea.String(d.config.ResourceId), + } + describeResourceInstanceCertsResp, err := d.sdkClient.DescribeResourceInstanceCertsWithContext(ctx, describeResourceInstanceCertsReq, &dara.RuntimeOptions{}) + d.logger.Debug("sdk request 'waf.DescribeResourceInstanceCerts'", slog.Any("request", describeResourceInstanceCertsReq), slog.Any("response", describeResourceInstanceCertsResp)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'waf.DescribeResourceInstanceCerts': %w", err) } - if d.config.Domain == "" { - // 未指定扩展域名,只需替换默认证书 - const certAppliedTypeDefault = "default" - for _, certItem := range modifyCloudResourceReq.Listen.Certificates { - if tea.StringValue(certItem.AppliedType) == certAppliedTypeDefault && - tea.StringValue(certItem.CertificateId) == cloudCertId { - return nil - } - } - modifyCloudResourceReq.Listen.Certificates = lo.Filter(modifyCloudResourceReq.Listen.Certificates, func(c *aliwaf.ModifyCloudResourceRequestListenCertificates, _ int) bool { - return tea.StringValue(c.AppliedType) != certAppliedTypeDefault - }) - modifyCloudResourceReq.Listen.Certificates = append(modifyCloudResourceReq.Listen.Certificates, &aliwaf.ModifyCloudResourceRequestListenCertificates{ - CertificateId: tea.String(cloudCertId), - AppliedType: tea.String(certAppliedTypeDefault), + if describeResourceInstanceCertsResp.Body == nil { + return fmt.Errorf("describe waf %s cloud resource instance certificates response body is empty", d.config.InstanceId) + } + + resourceInstanceCertificates = describeResourceInstanceCertsResp.Body.Certs + if len(resourceInstanceCertificates) == 0 { + return fmt.Errorf("describe waf %s cloud resource instance certificates response certs list is empty", d.config.InstanceId) + } + + // DescribeCloudResourceAccessPortDetails + // 获取云产品实例的接入端口详情,包含证书列表、实例 ID 等信息。 + // REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describecloudresourceaccessportdetails + var cloudResourceAccessPortDetailsCertificates = make([]*aliwaf.DescribeCloudResourceAccessPortDetailsResponseBodyAccessPortDetailsCertificates, 0) + describeCloudResourceAccessPortDetailsRequest := &aliwaf.DescribeCloudResourceAccessPortDetailsRequest{ + RegionId: tea.String(d.config.Region), + InstanceId: tea.String(d.config.InstanceId), + ResourceInstanceId: tea.String(d.config.ResourceId), + Port: tea.String(fmt.Sprintf("%d", d.config.ResourcePort)), + } + describeCloudResourceAccessPortDetailsResponse, err := d.sdkClient.DescribeCloudResourceAccessPortDetailsWithContext(ctx, describeCloudResourceAccessPortDetailsRequest, &dara.RuntimeOptions{}) + d.logger.Debug("sdk request 'waf.DescribeCloudResourceAccessPortDetails'", slog.Any("request", describeCloudResourceAccessPortDetailsRequest), slog.Any("response", describeCloudResourceAccessPortDetailsResponse)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'waf.DescribeCloudResourceAccessPortDetails': %w", err) + } + + if describeCloudResourceAccessPortDetailsResponse.Body.AccessPortDetails == nil { + return fmt.Errorf("could not get access port details of waf '%s' cloud resource '%s %s:%d'", d.config.InstanceId, d.config.ResourceProduct, d.config.ResourceId, d.config.ResourcePort) + } + + cloudResourceAccessPortDetailsCertificates = describeCloudResourceAccessPortDetailsResponse.Body.AccessPortDetails[0].Certificates + // 至少应该存在一张默认证书 + if len(cloudResourceAccessPortDetailsCertificates) == 0 { + return fmt.Errorf("could not get certificates of waf '%s' cloud resource '%s %s:%d'", d.config.InstanceId, d.config.ResourceProduct, d.config.ResourceId, d.config.ResourcePort) + } + + // 获取由 WAF 自动生成的 CloudResourceId,后续更新证书配置时需要用到 + cloudResourceId := describeCloudResourceAccessPortDetailsResponse.Body.AccessPortDetails[0].CloudResourceId + if cloudResourceId == nil || tea.StringValue(cloudResourceId) == "" { + return fmt.Errorf("could not get cloud resource id of waf '%s' cloud resource '%s %s:%d'", d.config.InstanceId, d.config.ResourceProduct, d.config.ResourceId, d.config.ResourcePort) + } + + // ModifyCloudResourceCert + // 更新证书配置 + // REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifycloudresourcecert + modifyCloudResourceCertReq := &aliwaf.ModifyCloudResourceCertRequest{ + RegionId: tea.String(d.config.Region), + InstanceId: tea.String(d.config.InstanceId), + CloudResourceId: cloudResourceId, + } + + if d.config.Domain == "" { + // 没有指定扩展域名,只需替换默认证书 + const certAppliedTypeDefault = "default" + + // 从已有列表里找出默认证书 ID,替换为新的证书 ID,其他的扩展证书保持不变(如果有) + modifyCloudResourceCertReq.Certificates = lo.Map(cloudResourceAccessPortDetailsCertificates, func(c *aliwaf.DescribeCloudResourceAccessPortDetailsResponseBodyAccessPortDetailsCertificates, _ int) *aliwaf.ModifyCloudResourceCertRequestCertificates { + appliedType := tea.StringValue(c.AppliedType) + certId := tea.StringValue(c.CertificateId) + if appliedType == certAppliedTypeDefault { + certId = cloudCertId + } + return &aliwaf.ModifyCloudResourceCertRequestCertificates{ + CertificateId: tea.String(certId), + AppliedType: tea.String(appliedType), + } }) } else { - // 指定扩展域名,需替换扩展证书 + // 指定了扩展域名,如果已有就替换,没有就新增扩展证书 const certAppliedTypeExtension = "extension" - modifyCloudResourceReq.Listen.Certificates = append(modifyCloudResourceReq.Listen.Certificates, &aliwaf.ModifyCloudResourceRequestListenCertificates{ - CertificateId: tea.String(cloudCertId), - AppliedType: tea.String(certAppliedTypeExtension), - }) + // 先拿到当前实例已有的扩展证书的列表 + // 构建新的证书列表,保留默认证书,处理扩展证书的替换或保留 + var found bool + newCertificates := make([]*aliwaf.ModifyCloudResourceCertRequestCertificates, 0, len(cloudResourceAccessPortDetailsCertificates)+1) + for _, c := range cloudResourceAccessPortDetailsCertificates { + applied := tea.StringValue(c.AppliedType) + certId := tea.StringValue(c.CertificateId) + + if applied == certAppliedTypeExtension { + // 从已同步的云产品实例全部证书列表里找到对应的证书,获取 CommonName + var certCommonName string + for _, rc := range resourceInstanceCertificates { + if tea.StringValue(rc.CertIdentifier) == certId { + certCommonName = tea.StringValue(rc.CommonName) + break + } + } + if certCommonName == d.config.Domain { + // 找到和目标域名匹配的扩展证书,替换为新证书 ID + newCertificates = append(newCertificates, &aliwaf.ModifyCloudResourceCertRequestCertificates{ + CertificateId: tea.String(cloudCertId), + AppliedType: tea.String(certAppliedTypeExtension), + }) + found = true + } else { + // 保留原来的扩展证书 + newCertificates = append(newCertificates, &aliwaf.ModifyCloudResourceCertRequestCertificates{ + CertificateId: c.CertificateId, + AppliedType: c.AppliedType, + }) + } + } else { + // 默认证书保持不变 + newCertificates = append(newCertificates, &aliwaf.ModifyCloudResourceCertRequestCertificates{ + CertificateId: c.CertificateId, + AppliedType: c.AppliedType, + }) + } + } + if !found { + // 如果没有找到要替换的扩展证书,追加一个扩展证书 + newCertificates = append(newCertificates, &aliwaf.ModifyCloudResourceCertRequestCertificates{ + CertificateId: tea.String(cloudCertId), + AppliedType: tea.String(certAppliedTypeExtension), + }) + } + + modifyCloudResourceCertReq.Certificates = newCertificates } // 过滤掉不存在或已过期的证书,防止接口报错 - modifyCloudResourceReq.Listen.Certificates = lo.Filter(modifyCloudResourceReq.Listen.Certificates, func(c *aliwaf.ModifyCloudResourceRequestListenCertificates, _ int) bool { + modifyCloudResourceCertReq.Certificates = lo.Filter(modifyCloudResourceCertReq.Certificates, func(c *aliwaf.ModifyCloudResourceCertRequestCertificates, _ int) bool { if tea.StringValue(c.CertificateId) == cloudCertId { return true } @@ -285,14 +349,24 @@ func (d *Deployer) deployToWAF3WithCloudResource(ctx context.Context, cloudCertI return false }) - // 修改云产品接入的配置 - // REF: https://www.alibabacloud.com/help/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifycloudresource - modifyCloudResourceResp, err := d.sdkClient.ModifyCloudResourceWithContext(ctx, modifyCloudResourceReq, &dara.RuntimeOptions{}) - d.logger.Debug("sdk request 'waf.ModifyCloudResource'", slog.Any("request", modifyCloudResourceReq), slog.Any("response", modifyCloudResourceResp)) - if err != nil { - return fmt.Errorf("failed to execute sdk request 'waf.ModifyCloudResource': %w", err) + // 最后检查待更新的列表里是否存在默认证书,没有则退出,否则接口会报错 + var hasDefaultCert bool + for _, c := range modifyCloudResourceCertReq.Certificates { + if tea.StringValue(c.AppliedType) == "default" { + hasDefaultCert = true + break + } + } + if !hasDefaultCert { + return fmt.Errorf("could not find valid default certificate to apply for waf '%s' cloud resource '%s %s:%d'", d.config.InstanceId, d.config.ResourceProduct, d.config.ResourceId, d.config.ResourcePort) } + // 执行更新请求 + modifyCloudResourceCertResp, err := d.sdkClient.ModifyCloudResourceCertWithContext(ctx, modifyCloudResourceCertReq, &dara.RuntimeOptions{}) + d.logger.Debug("sdk request 'waf.ModifyCloudResourceCert'", slog.Any("request", modifyCloudResourceCertReq), slog.Any("response", modifyCloudResourceCertResp)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'waf.ModifyCloudResourceCert': %w", err) + } return nil } diff --git a/pkg/core/deployer/providers/aliyun-waf/internal/client.go b/pkg/core/deployer/providers/aliyun-waf/internal/client.go index 00454aae3..3103b4f79 100644 --- a/pkg/core/deployer/providers/aliyun-waf/internal/client.go +++ b/pkg/core/deployer/providers/aliyun-waf/internal/client.go @@ -458,3 +458,178 @@ func (client *WafClient) ModifyDomainWithContext(ctx context.Context, tmpReq *al _err = dara.Convert(_body, &_result) return _result, _err } + +func (client *WafClient) CreateCertsWithContext(ctx context.Context, request *aliwaf.CreateCertsRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.CreateCertsResponse, _err error) { + _err = request.Validate() + if _err != nil { + return _result, _err + } + query := map[string]interface{}{} + if !dara.IsNil(request.CertContent) { + query["CertContent"] = request.CertContent + } + + if !dara.IsNil(request.CertKey) { + query["CertKey"] = request.CertKey + } + + if !dara.IsNil(request.CertName) { + query["CertName"] = request.CertName + } + + if !dara.IsNil(request.InstanceId) { + query["InstanceId"] = request.InstanceId + } + + if !dara.IsNil(request.RegionId) { + query["RegionId"] = request.RegionId + } + + if !dara.IsNil(request.ResourceManagerResourceGroupId) { + query["ResourceManagerResourceGroupId"] = request.ResourceManagerResourceGroupId + } + + req := &openapiutil.OpenApiRequest{ + Query: openapiutil.Query(query), + } + params := &openapiutil.Params{ + Action: dara.String("CreateCerts"), + Version: dara.String("2021-10-01"), + Protocol: dara.String("HTTPS"), + Pathname: dara.String("/"), + Method: dara.String("POST"), + AuthType: dara.String("AK"), + Style: dara.String("RPC"), + ReqBodyType: dara.String("formData"), + BodyType: dara.String("json"), + } + _result = &aliwaf.CreateCertsResponse{} + _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) + if _err != nil { + return _result, _err + } + _err = dara.Convert(_body, &_result) + return _result, _err +} + +func (client *WafClient) DescribeCloudResourceAccessPortDetailsWithContext(ctx context.Context, request *aliwaf.DescribeCloudResourceAccessPortDetailsRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.DescribeCloudResourceAccessPortDetailsResponse, _err error) { + _err = request.Validate() + if _err != nil { + return _result, _err + } + query := map[string]interface{}{} + if !dara.IsNil(request.InstanceId) { + query["InstanceId"] = request.InstanceId + } + + if !dara.IsNil(request.PageNumber) { + query["PageNumber"] = request.PageNumber + } + + if !dara.IsNil(request.PageSize) { + query["PageSize"] = request.PageSize + } + + if !dara.IsNil(request.Port) { + query["Port"] = request.Port + } + + if !dara.IsNil(request.Protocol) { + query["Protocol"] = request.Protocol + } + + if !dara.IsNil(request.RegionId) { + query["RegionId"] = request.RegionId + } + + if !dara.IsNil(request.ResourceInstanceId) { + query["ResourceInstanceId"] = request.ResourceInstanceId + } + + if !dara.IsNil(request.ResourceManagerResourceGroupId) { + query["ResourceManagerResourceGroupId"] = request.ResourceManagerResourceGroupId + } + + if !dara.IsNil(request.ResourceProduct) { + query["ResourceProduct"] = request.ResourceProduct + } + + req := &openapiutil.OpenApiRequest{ + Query: openapiutil.Query(query), + } + params := &openapiutil.Params{ + Action: dara.String("DescribeCloudResourceAccessPortDetails"), + Version: dara.String("2021-10-01"), + Protocol: dara.String("HTTPS"), + Pathname: dara.String("/"), + Method: dara.String("POST"), + AuthType: dara.String("AK"), + Style: dara.String("RPC"), + ReqBodyType: dara.String("formData"), + BodyType: dara.String("json"), + } + _result = &aliwaf.DescribeCloudResourceAccessPortDetailsResponse{} + _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) + if _err != nil { + return _result, _err + } + _err = dara.Convert(_body, &_result) + return _result, _err +} + +func (client *WafClient) ModifyCloudResourceCertWithContext(ctx context.Context, request *aliwaf.ModifyCloudResourceCertRequest, runtime *dara.RuntimeOptions) (_result *aliwaf.ModifyCloudResourceCertResponse, _err error) { + _err = request.Validate() + if _err != nil { + return _result, _err + } + query := map[string]interface{}{} + if !dara.IsNil(request.Certificates) { + query["Certificates"] = request.Certificates + } + + if !dara.IsNil(request.CloudResourceId) { + query["CloudResourceId"] = request.CloudResourceId + } + + if !dara.IsNil(request.InstanceId) { + query["InstanceId"] = request.InstanceId + } + + if !dara.IsNil(request.Port) { + query["Port"] = request.Port + } + + if !dara.IsNil(request.RegionId) { + query["RegionId"] = request.RegionId + } + + if !dara.IsNil(request.ResourceInstanceId) { + query["ResourceInstanceId"] = request.ResourceInstanceId + } + + if !dara.IsNil(request.ResourceProduct) { + query["ResourceProduct"] = request.ResourceProduct + } + + req := &openapiutil.OpenApiRequest{ + Query: openapiutil.Query(query), + } + params := &openapiutil.Params{ + Action: dara.String("ModifyCloudResourceCert"), + Version: dara.String("2021-10-01"), + Protocol: dara.String("HTTPS"), + Pathname: dara.String("/"), + Method: dara.String("POST"), + AuthType: dara.String("AK"), + Style: dara.String("RPC"), + ReqBodyType: dara.String("formData"), + BodyType: dara.String("json"), + } + _result = &aliwaf.ModifyCloudResourceCertResponse{} + _body, _err := client.CallApiWithCtx(ctx, params, req, runtime) + if _err != nil { + return _result, _err + } + _err = dara.Convert(_body, &_result) + return _result, _err +}