Files
cloudpods/pkg/compute/models/loadbalancercertificates.go
2020-04-01 20:16:52 +08:00

468 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2019 Yunion
//
// 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.
package models
import (
"context"
"crypto/sha256"
"crypto/x509"
"database/sql"
"encoding/hex"
"encoding/pem"
"fmt"
"strings"
"time"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/apis"
api "yunion.io/x/onecloud/pkg/apis/compute"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/validators"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/stringutils2"
)
type SLoadbalancerCertificateManager struct {
SLoadbalancerLogSkipper
db.SVirtualResourceBaseManager
db.SExternalizedResourceBaseManager
}
var LoadbalancerCertificateManager *SLoadbalancerCertificateManager
func init() {
LoadbalancerCertificateManager = &SLoadbalancerCertificateManager{
SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager(
SLoadbalancerCertificate{},
"loadbalancercertificates_tbl",
"loadbalancercertificate",
"loadbalancercertificates",
),
}
LoadbalancerCertificateManager.SetVirtualObject(LoadbalancerCertificateManager)
}
// TODO
//
// - notify users of cert expiration
// - ca info: self-signed, public ca
type SLoadbalancerCertificate struct {
db.SVirtualResourceBase
db.SExternalizedResourceBase
// SManagedResourceBase
// SCloudregionResourceBase
Certificate string `create:"required" list:"user" update:"user"`
PrivateKey string `create:"required" list:"admin" update:"user"`
// derived attributes
PublicKeyAlgorithm string `create:"optional" list:"user" update:"user"`
PublicKeyBitLen int `create:"optional" list:"user" update:"user"`
SignatureAlgorithm string `create:"optional" list:"user" update:"user"`
Fingerprint string `create:"optional" list:"user" update:"user"`
NotBefore time.Time `create:"optional" list:"user" update:"user"`
NotAfter time.Time `create:"optional" list:"user" update:"user"`
CommonName string `create:"optional" list:"user" update:"user"`
SubjectAlternativeNames string `create:"optional" list:"user" update:"user"`
}
func (lbcert *SLoadbalancerCertificate) GetCachedCerts() ([]SCachedLoadbalancerCertificate, error) {
ret := []SCachedLoadbalancerCertificate{}
q := CachedLoadbalancerCertificateManager.Query().Equals("certificate_id", lbcert.Id)
err := db.FetchModelObjects(CachedLoadbalancerCertificateManager, q, &ret)
if err != nil {
return nil, err
}
return ret, nil
}
func (lbcert *SLoadbalancerCertificate) AllowPerformStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return false
}
func (lbcert *SLoadbalancerCertificate) ValidateUpdateData(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
if data.Contains("certificate") || data.Contains("private_key") {
return nil, httperrors.NewForbiddenError("not allowed update content of certificate")
}
updateData := jsonutils.NewDict()
if name, err := data.GetString("name"); err == nil {
updateData.Set("name", jsonutils.NewString(name))
}
if desc, err := data.GetString("description"); err == nil {
updateData.Set("description", jsonutils.NewString(desc))
}
input := apis.VirtualResourceBaseUpdateInput{}
err := updateData.Unmarshal(&input)
if err != nil {
return nil, errors.Wrap(err, "Unmarshal")
}
input, err = lbcert.SVirtualResourceBase.ValidateUpdateData(ctx, userCred, query, input)
if err != nil {
return nil, errors.Wrap(err, "SVirtualResourceBase.ValidateUpdateData")
}
updateData.Update(jsonutils.Marshal(input))
return updateData, nil
}
func (lbcert *SLoadbalancerCertificate) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerProjId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
lbcert.SVirtualResourceBase.PostCreate(ctx, userCred, ownerProjId, query, data)
lbcert.SetStatus(userCred, api.LB_STATUS_ENABLED, "")
}
func (lbcert *SLoadbalancerCertificate) GetExtraDetails(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
isList bool,
) (api.LoadbalancerCertificateDetails, error) {
return api.LoadbalancerCertificateDetails{}, nil
}
func (manager *SLoadbalancerCertificateManager) FetchCustomizeColumns(
ctx context.Context,
userCred mcclient.TokenCredential,
query jsonutils.JSONObject,
objs []interface{},
fields stringutils2.SSortedStrings,
isList bool,
) []api.LoadbalancerCertificateDetails {
rows := make([]api.LoadbalancerCertificateDetails, len(objs))
virtRows := manager.SVirtualResourceBaseManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)
for i := range rows {
rows[i] = api.LoadbalancerCertificateDetails{
VirtualResourceDetails: virtRows[i],
}
}
return rows
}
func (lbcert *SLoadbalancerCertificate) ValidateDeleteCondition(ctx context.Context) error {
men := []db.IModelManager{
LoadbalancerListenerManager,
CachedLoadbalancerCertificateManager,
}
lbcertId := lbcert.Id
for _, man := range men {
t := man.TableSpec().Instance()
pdF := t.Field("pending_deleted")
n, err := t.Query().
Equals("certificate_id", lbcertId).
Filter(sqlchemy.OR(sqlchemy.IsNull(pdF), sqlchemy.IsFalse(pdF))).
CountWithError()
if err != nil {
return httperrors.NewInternalServerError("get certificate refcount fail %s", err)
}
if n > 0 {
return httperrors.NewResourceBusyError("certificate %s is still referred to by %d %s",
lbcertId, n, man.KeywordPlural())
}
}
return nil
}
func (lbcert *SLoadbalancerCertificate) AllowPerformPurge(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) bool {
return db.IsAdminAllowPerform(userCred, lbcert, "purge")
}
func (lbcert *SLoadbalancerCertificate) PerformPurge(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
return nil, lbcert.CustomizeDelete(ctx, userCred, query, data)
}
func (lbcert *SLoadbalancerCertificate) Delete(ctx context.Context, userCred mcclient.TokenCredential) error {
if !lbcert.PendingDeleted {
return lbcert.DoPendingDelete(ctx, userCred)
}
return nil
}
func (man *SLoadbalancerCertificateManager) validateCertKey(ctx context.Context, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
certV := validators.NewCertificateValidator("certificate")
pkeyV := validators.NewPrivateKeyValidator("private_key")
keyV := map[string]validators.IValidator{
"certificate": certV,
"private_key": pkeyV,
}
for _, v := range keyV {
if err := v.Validate(data); err != nil {
return nil, err
}
}
cert := certV.Certificates[0]
var certPubKeyAlgo string
{
// x509.PublicKeyAlgorithm.String() is only available since go1.10
switch cert.PublicKeyAlgorithm {
case x509.RSA:
certPubKeyAlgo = api.LB_TLS_CERT_PUBKEY_ALGO_RSA
case x509.ECDSA:
certPubKeyAlgo = api.LB_TLS_CERT_PUBKEY_ALGO_ECDSA
default:
certPubKeyAlgo = fmt.Sprintf("algo %#v", cert.PublicKeyAlgorithm)
}
if !api.LB_TLS_CERT_PUBKEY_ALGOS.Has(certPubKeyAlgo) {
return nil, httperrors.NewInputParameterError("invalid cert pubkey algorithm: %s, want %s",
certPubKeyAlgo, api.LB_TLS_CERT_PUBKEY_ALGOS.String())
}
}
err := pkeyV.MatchCertificate(cert)
if err != nil {
return nil, err
}
// NOTE subject alternative names also includes email, url, ip addresses,
// but we ignore them here.
//
// NOTE we use white space to separate names
data.Set("common_name", jsonutils.NewString(cert.Subject.CommonName))
data.Set("subject_alternative_names", jsonutils.NewString(strings.Join(cert.DNSNames, " ")))
data.Set("not_before", jsonutils.NewTimeString(cert.NotBefore))
data.Set("not_after", jsonutils.NewTimeString(cert.NotAfter))
data.Set("public_key_algorithm", jsonutils.NewString(certPubKeyAlgo))
data.Set("public_key_bit_len", jsonutils.NewInt(int64(certV.PublicKeyBitLen())))
data.Set("signature_algorithm", jsonutils.NewString(cert.SignatureAlgorithm.String()))
data.Set("fingerprint", jsonutils.NewString(api.LB_TLS_CERT_FINGERPRINT_ALGO_SHA256+":"+certV.FingerprintSha256String()))
return data, nil
}
func (man *SLoadbalancerCertificateManager) ListItemFilter(
ctx context.Context,
q *sqlchemy.SQuery,
userCred mcclient.TokenCredential,
query api.LoadbalancerCertificateListInput,
) (*sqlchemy.SQuery, error) {
q, err := man.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, query.VirtualResourceListInput)
if err != nil {
return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemFilter")
}
q, err = man.SExternalizedResourceBaseManager.ListItemFilter(ctx, q, userCred, query.ExternalizedResourceBaseListInput)
if err != nil {
return nil, errors.Wrap(err, "SExternalizedResourceBaseManager.ListItemFilter")
}
if query.Usable != nil && *query.Usable {
region := query.Cloudregion
manager := query.Cloudprovider
// 证书可用包含两类1.本地证书内容不为空 2.公有云中已经存在,但是证书内容不完整的证书
if len(region) > 0 || len(manager) > 0 {
q2 := CachedLoadbalancerCertificateManager.Query("certificate_id").IsFalse("pending_deleted")
if len(region) > 0 {
q2 = q2.Equals("cloudregion_id", region)
}
if len(manager) > 0 {
q2 = q2.Equals("manager_id", manager)
}
count, err := q2.CountWithError()
if err != nil && err != sql.ErrNoRows {
return nil, err
}
if count > 0 {
conditionA := sqlchemy.AND(sqlchemy.IsNotEmpty(q.Field("certificate")), sqlchemy.IsNotEmpty(q.Field("private_key")))
conditionB := sqlchemy.In(q.Field("id"), q2.SubQuery())
q = q.Filter(sqlchemy.OR(conditionA, conditionB))
} else {
q = q.IsNotEmpty("certificate").IsNotEmpty("private_key")
}
} else {
q = q.IsNotEmpty("certificate").IsNotEmpty("private_key")
}
}
if len(query.CommonName) > 0 {
q = q.In("common_name", query.CommonName)
}
if len(query.SubjectAlternativeNames) > 0 {
q = q.In("subject_alternative_names", query.SubjectAlternativeNames)
}
return q, nil
}
func (man *SLoadbalancerCertificateManager) OrderByExtraFields(
ctx context.Context,
q *sqlchemy.SQuery,
userCred mcclient.TokenCredential,
query api.LoadbalancerCertificateListInput,
) (*sqlchemy.SQuery, error) {
var err error
q, err = man.SVirtualResourceBaseManager.OrderByExtraFields(ctx, q, userCred, query.VirtualResourceListInput)
if err != nil {
return nil, errors.Wrap(err, "SVirtualResourceBaseManager.OrderByExtraFields")
}
return q, nil
}
func (man *SLoadbalancerCertificateManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) {
var err error
q, err = man.SVirtualResourceBaseManager.QueryDistinctExtraField(q, field)
if err == nil {
return q, nil
}
return q, httperrors.ErrNotFound
}
func (man *SLoadbalancerCertificateManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) {
data, err := man.validateCertKey(ctx, data)
if err != nil {
return nil, err
}
input := apis.VirtualResourceCreateInput{}
err = data.Unmarshal(&input)
if err != nil {
return nil, httperrors.NewInternalServerError("unmarshal VirtualResourceCreateInput fail %s", err)
}
input, err = man.SVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input)
if err != nil {
return nil, err
}
data.Update(jsonutils.Marshal(input))
data.Remove("cloudregion_id")
data.Remove("manager_id")
return data, nil
}
func (man *SLoadbalancerCertificateManager) InitializeData() error {
// initialize newly added null certificate fingerprint column
q := man.Query().IsNull("fingerprint")
lbcerts := []SLoadbalancerCertificate{}
if err := q.All(&lbcerts); err != nil {
return err
}
for i := range lbcerts {
lbcert := &lbcerts[i]
fp := lbcert.Fingerprint
if fp != "" {
continue
}
if lbcert.Certificate == "" {
continue
}
{
p, _ := pem.Decode([]byte(lbcert.Certificate))
c, err := x509.ParseCertificate(p.Bytes)
if err != nil {
log.Errorf("parsing certificate %s(%s): %s", lbcert.Name, lbcert.Id, err)
continue
}
d := sha256.Sum256(c.Raw)
fp = api.LB_TLS_CERT_FINGERPRINT_ALGO_SHA256 + ":" + hex.EncodeToString(d[:])
}
_, err := db.Update(lbcert, func() error {
lbcert.Fingerprint = fp
return nil
})
if err != nil {
return err
}
}
// sync certificate to certificate cache
/*lbcerts = []SLoadbalancerCertificate{}
cachedCerts := CachedLoadbalancerCertificateManager.Query("certificate_id").SubQuery()
q2 := man.Query().IsNotEmpty("external_id").IsNotEmpty("cloudregion_id").NotIn("id", cachedCerts)
if err := q2.All(&lbcerts); err != nil {
return err
}
for i := range lbcerts {
cert := lbcerts[i]
certObj := jsonutils.Marshal(cert)
cachedCert := &SCachedLoadbalancerCertificate{}
err := certObj.Unmarshal(cachedCert)
if err != nil {
return err
}
cachedCert.Id = ""
cachedCert.CertificateId = cert.Id
err = CachedLoadbalancerCertificateManager.TableSpec().Insert(cachedCert)
if err != nil {
return err
}
}*/
return nil
}
func (man *SLoadbalancerCertificateManager) CreateCertificate(userCred mcclient.TokenCredential, name string, publicKey string, privateKey, fingerprint string) (*SLoadbalancerCertificate, error) {
if len(fingerprint) == 0 {
return nil, fmt.Errorf("CreateCertificate fingerprint can not be empty")
}
data := jsonutils.NewDict()
data.Set("certificate", jsonutils.NewString(publicKey))
data.Set("private_key", jsonutils.NewString(privateKey))
data.Set("name", jsonutils.NewString(name))
data.Set("fingerprint", jsonutils.NewString(fingerprint))
q := man.Query().Equals("fingerprint", fingerprint).Asc("created_at").IsFalse("pending_deleted")
count, err := q.CountWithError()
if err != nil && err != sql.ErrNoRows {
return nil, err
}
if count == 0 {
cert := &SLoadbalancerCertificate{}
err := data.Unmarshal(cert)
if err != nil {
return nil, err
}
// usercred
cert.DomainId = userCred.GetProjectDomainId()
cert.ProjectId = userCred.GetProjectId()
cert.ProjectSrc = string(apis.OWNER_SOURCE_CLOUD)
err = man.TableSpec().Insert(cert)
if err != nil {
return nil, err
}
}
ret := &SLoadbalancerCertificate{}
err = q.First(ret)
if err != nil {
return nil, err
}
return ret, nil
}
func (manager *SLoadbalancerCertificateManager) GetResourceCount() ([]db.SScopeResourceCount, error) {
virts := manager.Query().IsFalse("pending_deleted")
return db.CalculateProjectResourceCount(virts)
}