From 8556eaedcae121f3f4995cb8ce8f24267bae0c04 Mon Sep 17 00:00:00 2001 From: Phil Porada Date: Wed, 20 Mar 2024 13:08:31 -0400 Subject: [PATCH] SA: store and return certificate profile name (#7352) Adds `certificateProfileName` to the `orders` database table. The [maximum length](https://github.com/letsencrypt/boulder/pull/7325/files#diff-a64a0af7cbf484da8e6d08d3eefdeef9314c5d9888233f0adcecd21b800102acR35) of a profile name matches the `//issuance` package. Adds a `MultipleCertificateProfiles` feature flag that, when enabled, will store the certificate profile name from a `NewOrderRequest`. The certificate profile name is allowed to be empty and the database will treat that row as [NULL](https://mariadb.com/kb/en/null-values/). When the SA retrieves this potentially NULL row, it will be cast as the golang string zero value `""`. SRE ticket IN-10145 has been filed to perform the database migration and enable the new feature flag. The migration must be performed before enabling the feature flag. Part of https://github.com/letsencrypt/boulder/issues/7324 --- features/features.go | 8 + mocks/mocks.go | 24 +-- sa/database.go | 10 +- .../20240304000000_CertificateProfiles.sql | 9 ++ sa/model.go | 70 ++++++++- sa/model_test.go | 42 ++++- sa/sa.go | 45 ++++-- sa/sa_test.go | 147 +++++++++++++++++- sa/saro.go | 16 +- test/config-next/sa.json | 1 + 10 files changed, 336 insertions(+), 36 deletions(-) create mode 100644 sa/db-next/boulder_sa/20240304000000_CertificateProfiles.sql diff --git a/features/features.go b/features/features.go index 58763b2bce4..ffc41e5a333 100644 --- a/features/features.go +++ b/features/features.go @@ -89,6 +89,14 @@ type Config struct { // a 'orderID' matching the finalized order to true. This will occur // inside of the finalize (order) transaction. TrackReplacementCertificatesARI bool + + // MultipleCertificateProfiles, when enabled, triggers the following + // behavior: + // - SA.NewOrderAndAuthzs: upon receiving a NewOrderRequest with a + // `certificateProfileName` value, will add that value to the database's + // `orders.certificateProfileName` column. Values in this column are + // allowed to be empty. + MultipleCertificateProfiles bool } var fMu = new(sync.RWMutex) diff --git a/mocks/mocks.go b/mocks/mocks.go index 4729bb31321..dc8f7eff6e1 100644 --- a/mocks/mocks.go +++ b/mocks/mocks.go @@ -387,8 +387,9 @@ func (sa *StorageAuthority) NewOrderAndAuthzs(_ context.Context, req *sapb.NewOr Id: rand.Int63(), Created: timestamppb.Now(), // A new order is never processing because it can't have been finalized yet. - BeganProcessing: false, - Status: string(core.StatusPending), + BeganProcessing: false, + Status: string(core.StatusPending), + CertificateProfileName: req.NewOrder.CertificateProfileName, } return response, nil } @@ -420,15 +421,16 @@ func (sa *StorageAuthorityReadOnly) GetOrder(_ context.Context, req *sapb.OrderR created := now.AddDate(-30, 0, 0) exp := now.AddDate(30, 0, 0) validOrder := &corepb.Order{ - Id: req.Id, - RegistrationID: 1, - Created: timestamppb.New(created), - Expires: timestamppb.New(exp), - Names: []string{"example.com"}, - Status: string(core.StatusValid), - V2Authorizations: []int64{1}, - CertificateSerial: "serial", - Error: nil, + Id: req.Id, + RegistrationID: 1, + Created: timestamppb.New(created), + Expires: timestamppb.New(exp), + Names: []string{"example.com"}, + Status: string(core.StatusValid), + V2Authorizations: []int64{1}, + CertificateSerial: "serial", + Error: nil, + CertificateProfileName: "defaultBoulderCertificateProfile", } // Order ID doesn't have a certificate serial yet diff --git a/sa/database.go b/sa/database.go index 5997f47d652..43f5c5eaa51 100644 --- a/sa/database.go +++ b/sa/database.go @@ -6,12 +6,14 @@ import ( "time" "github.com/go-sql-driver/mysql" - "github.com/letsencrypt/borp" "github.com/prometheus/client_golang/prometheus" + "github.com/letsencrypt/borp" + "github.com/letsencrypt/boulder/cmd" "github.com/letsencrypt/boulder/core" boulderDB "github.com/letsencrypt/boulder/db" + "github.com/letsencrypt/boulder/features" blog "github.com/letsencrypt/boulder/log" ) @@ -271,7 +273,11 @@ func initTables(dbMap *borp.DbMap) { dbMap.AddTableWithName(core.Certificate{}, "certificates").SetKeys(true, "ID") dbMap.AddTableWithName(core.CertificateStatus{}, "certificateStatus").SetKeys(true, "ID") dbMap.AddTableWithName(core.FQDNSet{}, "fqdnSets").SetKeys(true, "ID") - dbMap.AddTableWithName(orderModel{}, "orders").SetKeys(true, "ID") + if features.Get().MultipleCertificateProfiles { + dbMap.AddTableWithName(orderModelv2{}, "orders").SetKeys(true, "ID") + } else { + dbMap.AddTableWithName(orderModelv1{}, "orders").SetKeys(true, "ID") + } dbMap.AddTableWithName(orderToAuthzModel{}, "orderToAuthz").SetKeys(false, "OrderID", "AuthzID") dbMap.AddTableWithName(requestedNameModel{}, "requestedNames").SetKeys(false, "OrderID") dbMap.AddTableWithName(orderFQDNSet{}, "orderFqdnSets").SetKeys(true, "ID") diff --git a/sa/db-next/boulder_sa/20240304000000_CertificateProfiles.sql b/sa/db-next/boulder_sa/20240304000000_CertificateProfiles.sql new file mode 100644 index 00000000000..583a106d6b8 --- /dev/null +++ b/sa/db-next/boulder_sa/20240304000000_CertificateProfiles.sql @@ -0,0 +1,9 @@ +-- +migrate Up +-- SQL in section 'Up' is executed when this migration is applied + +ALTER TABLE `orders` ADD COLUMN `certificateProfileName` varchar(32) DEFAULT NULL; + +-- +migrate Down +-- SQL section 'Down' is executed when this migration is rolled back + +ALTER TABLE `orders` DROP COLUMN `certificateProfileName`; diff --git a/sa/model.go b/sa/model.go index 33dc14c242d..099f6f8bf0b 100644 --- a/sa/model.go +++ b/sa/model.go @@ -373,7 +373,8 @@ type precertificateModel struct { Expires time.Time } -type orderModel struct { +// TODO(#7324) orderModelv1 is deprecated, use orderModelv2 moving forward. +type orderModelv1 struct { ID int64 RegistrationID int64 Expires time.Time @@ -383,6 +384,17 @@ type orderModel struct { BeganProcessing bool } +type orderModelv2 struct { + ID int64 + RegistrationID int64 + Expires time.Time + Created time.Time + Error []byte + CertificateSerial string + BeganProcessing bool + CertificateProfileName string +} + type requestedNameModel struct { ID int64 OrderID int64 @@ -394,8 +406,9 @@ type orderToAuthzModel struct { AuthzID int64 } -func orderToModel(order *corepb.Order) (*orderModel, error) { - om := &orderModel{ +// TODO(#7324) orderToModelv1 is deprecated, use orderModelv2 moving forward. +func orderToModelv1(order *corepb.Order) (*orderModelv1, error) { + om := &orderModelv1{ ID: order.Id, RegistrationID: order.RegistrationID, Expires: order.Expires.AsTime(), @@ -417,7 +430,8 @@ func orderToModel(order *corepb.Order) (*orderModel, error) { return om, nil } -func modelToOrder(om *orderModel) (*corepb.Order, error) { +// TODO(#7324) modelToOrderv1 is deprecated, use orderModelv2 moving forward. +func modelToOrderv1(om *orderModelv1) (*corepb.Order, error) { order := &corepb.Order{ Id: om.ID, RegistrationID: om.RegistrationID, @@ -440,6 +454,54 @@ func modelToOrder(om *orderModel) (*corepb.Order, error) { return order, nil } +func orderToModelv2(order *corepb.Order) (*orderModelv2, error) { + om := &orderModelv2{ + ID: order.Id, + RegistrationID: order.RegistrationID, + Expires: order.Expires.AsTime(), + Created: order.Created.AsTime(), + BeganProcessing: order.BeganProcessing, + CertificateSerial: order.CertificateSerial, + CertificateProfileName: order.CertificateProfileName, + } + + if order.Error != nil { + errJSON, err := json.Marshal(order.Error) + if err != nil { + return nil, err + } + if len(errJSON) > mediumBlobSize { + return nil, fmt.Errorf("Error object is too large to store in the database") + } + om.Error = errJSON + } + return om, nil +} + +func modelToOrderv2(om *orderModelv2) (*corepb.Order, error) { + order := &corepb.Order{ + Id: om.ID, + RegistrationID: om.RegistrationID, + Expires: timestamppb.New(om.Expires), + Created: timestamppb.New(om.Created), + CertificateSerial: om.CertificateSerial, + BeganProcessing: om.BeganProcessing, + CertificateProfileName: om.CertificateProfileName, + } + if len(om.Error) > 0 { + var problem corepb.ProblemDetails + err := json.Unmarshal(om.Error, &problem) + if err != nil { + return &corepb.Order{}, badJSONError( + "failed to unmarshal order model's error", + om.Error, + err) + } + order.Error = &problem + } + return order, nil +} + var challTypeToUint = map[string]uint8{ "http-01": 0, "dns-01": 1, diff --git a/sa/model_test.go b/sa/model_test.go index d109753ff6e..23f4e3754ac 100644 --- a/sa/model_test.go +++ b/sa/model_test.go @@ -16,12 +16,13 @@ import ( "time" "github.com/jmhodges/clock" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/letsencrypt/boulder/db" "github.com/letsencrypt/boulder/features" "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/probs" "github.com/letsencrypt/boulder/test/vars" - "google.golang.org/protobuf/types/known/timestamppb" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" @@ -248,15 +249,50 @@ func TestAuthzModel(t *testing.T) { // validation error JSON field to an Order produces the expected bad JSON error. func TestModelToOrderBadJSON(t *testing.T) { badJSON := []byte(`{`) - _, err := modelToOrder(&orderModel{ + _, err := modelToOrderv2(&orderModelv2{ Error: badJSON, }) - test.AssertError(t, err, "expected error from modelToOrder") + test.AssertError(t, err, "expected error from modelToOrderv2") var badJSONErr errBadJSON test.AssertErrorWraps(t, err, &badJSONErr) test.AssertEquals(t, string(badJSONErr.json), string(badJSON)) } +func TestOrderModelThereAndBackAgain(t *testing.T) { + clk := clock.New() + now := clk.Now() + order := &corepb.Order{ + Id: 0, + RegistrationID: 2016, + Expires: timestamppb.New(now.Add(24 * time.Hour)), + Created: timestamppb.New(now), + Error: nil, + CertificateSerial: "1", + BeganProcessing: true, + } + model1, err := orderToModelv1(order) + test.AssertNotError(t, err, "orderToModelv1 should not have errored") + returnOrder, err := modelToOrderv1(model1) + test.AssertNotError(t, err, "modelToOrderv1 should not have errored") + test.AssertDeepEquals(t, order, returnOrder) + + anotherOrder := &corepb.Order{ + Id: 1, + RegistrationID: 2024, + Expires: timestamppb.New(now.Add(24 * time.Hour)), + Created: timestamppb.New(now), + Error: nil, + CertificateSerial: "2", + BeganProcessing: true, + CertificateProfileName: "phljny", + } + model2, err := orderToModelv2(anotherOrder) + test.AssertNotError(t, err, "orderToModelv2 should not have errored") + returnOrder, err = modelToOrderv2(model2) + test.AssertNotError(t, err, "modelToOrderv2 should not have errored") + test.AssertDeepEquals(t, anotherOrder, returnOrder) +} + // TestPopulateAttemptedFieldsBadJSON tests that populating a challenge from an // authz2 model with an invalid validation error or an invalid validation record // produces the expected bad JSON error. diff --git a/sa/sa.go b/sa/sa.go index e5f38b805b6..cffcb61491b 100644 --- a/sa/sa.go +++ b/sa/sa.go @@ -498,12 +498,27 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb } // Second, insert the new order. - order := &orderModel{ - RegistrationID: req.NewOrder.RegistrationID, - Expires: req.NewOrder.Expires.AsTime(), - Created: ssa.clk.Now(), + var orderID int64 + var err error + created := ssa.clk.Now() + if features.Get().MultipleCertificateProfiles { + omv2 := orderModelv2{ + RegistrationID: req.NewOrder.RegistrationID, + Expires: req.NewOrder.Expires.AsTime(), + Created: created, + CertificateProfileName: req.NewOrder.CertificateProfileName, + } + err = tx.Insert(ctx, &omv2) + orderID = omv2.ID + } else { + omv1 := orderModelv1{ + RegistrationID: req.NewOrder.RegistrationID, + Expires: req.NewOrder.Expires.AsTime(), + Created: created, + } + err = tx.Insert(ctx, &omv1) + orderID = omv1.ID } - err := tx.Insert(ctx, order) if err != nil { return nil, err } @@ -514,13 +529,13 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb return nil, err } for _, id := range req.NewOrder.V2Authorizations { - err = inserter.Add([]interface{}{order.ID, id}) + err := inserter.Add([]interface{}{orderID, id}) if err != nil { return nil, err } } for _, id := range newAuthzIDs { - err = inserter.Add([]interface{}{order.ID, id}) + err := inserter.Add([]interface{}{orderID, id}) if err != nil { return nil, err } @@ -536,7 +551,7 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb return nil, err } for _, name := range req.NewOrder.Names { - err = inserter.Add([]interface{}{order.ID, ReverseName(name)}) + err := inserter.Add([]interface{}{orderID, ReverseName(name)}) if err != nil { return nil, err } @@ -547,7 +562,7 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb } // Fifth, insert the FQDNSet entry for the order. - err = addOrderFQDNSet(ctx, tx, req.NewOrder.Names, order.ID, order.RegistrationID, order.Expires) + err = addOrderFQDNSet(ctx, tx, req.NewOrder.Names, orderID, req.NewOrder.RegistrationID, req.NewOrder.Expires.AsTime()) if err != nil { return nil, err } @@ -555,8 +570,8 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb // Finally, build the overall Order PB. res := &corepb.Order{ // ID and Created were auto-populated on the order model when it was inserted. - Id: order.ID, - Created: timestamppb.New(order.Created), + Id: orderID, + Created: timestamppb.New(created), // These are carried over from the original request unchanged. RegistrationID: req.NewOrder.RegistrationID, Expires: req.NewOrder.Expires, @@ -565,12 +580,16 @@ func (ssa *SQLStorageAuthority) NewOrderAndAuthzs(ctx context.Context, req *sapb V2Authorizations: append(req.NewOrder.V2Authorizations, newAuthzIDs...), // A new order is never processing because it can't be finalized yet. BeganProcessing: false, + // An empty string is allowed. When the RA retrieves the order and + // transmits it to the CA, the empty string will take the value of + // DefaultCertProfileName from the //issuance package. + CertificateProfileName: req.NewOrder.CertificateProfileName, } if req.NewOrder.ReplacesSerial != "" { // Update the replacementOrders table to indicate that this order // replaces the provided certificate serial. - err := addReplacementOrder(ctx, tx, req.NewOrder.ReplacesSerial, order.ID, order.Expires) + err := addReplacementOrder(ctx, tx, req.NewOrder.ReplacesSerial, orderID, req.NewOrder.Expires.AsTime()) if err != nil { return nil, err } @@ -643,7 +662,7 @@ func (ssa *SQLStorageAuthority) SetOrderError(ctx context.Context, req *sapb.Set return nil, errIncompleteRequest } _, overallError := db.WithTransaction(ctx, ssa.dbMap, func(tx db.Executor) (interface{}, error) { - om, err := orderToModel(&corepb.Order{ + om, err := orderToModelv2(&corepb.Order{ Id: req.Id, Error: req.Error, }) diff --git a/sa/sa_test.go b/sa/sa_test.go index 5b99c12a94b..c1bd5dd6e4f 100644 --- a/sa/sa_test.go +++ b/sa/sa_test.go @@ -1520,7 +1520,7 @@ func TestFinalizeOrder(t *testing.T) { test.AssertEquals(t, updatedOrder.Status, string(core.StatusValid)) } -func TestOrder(t *testing.T) { +func TestOrderWithOrderModelv1(t *testing.T) { sa, fc, cleanup := initSA(t) defer cleanup() @@ -1584,6 +1584,151 @@ func TestOrder(t *testing.T) { test.AssertDeepEquals(t, storedOrder, expectedOrder) } +func TestOrderWithOrderModelv2(t *testing.T) { + if !strings.Contains(os.Getenv("BOULDER_CONFIG_DIR"), "test/config-next") { + t.Skip() + } + + // The feature must be set before the SA is constructed because of a + // conditional on this feature in //sa/database.go. + features.Set(features.Config{MultipleCertificateProfiles: true}) + defer features.Reset() + + fc := clock.NewFake() + fc.Set(time.Date(2015, 3, 4, 5, 0, 0, 0, time.UTC)) + + dbMap, err := DBMapForTest(vars.DBConnSA) + test.AssertNotError(t, err, "Couldn't create dbMap") + + saro, err := NewSQLStorageAuthorityRO(dbMap, nil, metrics.NoopRegisterer, 1, 0, fc, log) + test.AssertNotError(t, err, "Couldn't create SARO") + + sa, err := NewSQLStorageAuthorityWrapping(saro, dbMap, metrics.NoopRegisterer) + test.AssertNotError(t, err, "Couldn't create SA") + defer test.ResetBoulderTestDatabase(t) + + // Create a test registration to reference + key, _ := jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(1), E: 1}}.MarshalJSON() + initialIP, _ := net.ParseIP("42.42.42.42").MarshalText() + reg, err := sa.NewRegistration(ctx, &corepb.Registration{ + Key: key, + InitialIP: initialIP, + }) + test.AssertNotError(t, err, "Couldn't create test registration") + + authzExpires := fc.Now().Add(time.Hour) + authzID := createPendingAuthorization(t, sa, "example.com", authzExpires) + + // Set the order to expire in two hours + expires := fc.Now().Add(2 * time.Hour) + + inputOrder := &corepb.Order{ + RegistrationID: reg.Id, + Expires: timestamppb.New(expires), + Names: []string{"example.com"}, + V2Authorizations: []int64{authzID}, + CertificateProfileName: "tbiapb", + } + + // Create the order + order, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ + NewOrder: &sapb.NewOrderRequest{ + RegistrationID: inputOrder.RegistrationID, + Expires: inputOrder.Expires, + Names: inputOrder.Names, + V2Authorizations: inputOrder.V2Authorizations, + CertificateProfileName: inputOrder.CertificateProfileName, + }, + }) + test.AssertNotError(t, err, "sa.NewOrderAndAuthzs failed") + + // The Order from GetOrder should match the following expected order + created := sa.clk.Now() + expectedOrder := &corepb.Order{ + // The registration ID, authorizations, expiry, and names should match the + // input to NewOrderAndAuthzs + RegistrationID: inputOrder.RegistrationID, + V2Authorizations: inputOrder.V2Authorizations, + Names: inputOrder.Names, + Expires: inputOrder.Expires, + // The ID should have been set to 1 by the SA + Id: 1, + // The status should be pending + Status: string(core.StatusPending), + // The serial should be empty since this is a pending order + CertificateSerial: "", + // We should not be processing it + BeganProcessing: false, + // The created timestamp should have been set to the current time + Created: timestamppb.New(created), + CertificateProfileName: "tbiapb", + } + + // Fetch the order by its ID and make sure it matches the expected + storedOrder, err := sa.GetOrder(context.Background(), &sapb.OrderRequest{Id: order.Id}) + test.AssertNotError(t, err, "sa.GetOrder failed") + test.AssertDeepEquals(t, storedOrder, expectedOrder) + + // + // Test that an order without a certificate profile name, but with the + // MultipleCertificateProfiles feature flag enabled works as expected. + // + + // Create a test registration to reference + key2, _ := jose.JSONWebKey{Key: &rsa.PublicKey{N: big.NewInt(2), E: 2}}.MarshalJSON() + initialIP2, _ := net.ParseIP("44.44.44.44").MarshalText() + reg2, err := sa.NewRegistration(ctx, &corepb.Registration{ + Key: key2, + InitialIP: initialIP2, + }) + test.AssertNotError(t, err, "Couldn't create test registration") + + inputOrderNoName := &corepb.Order{ + RegistrationID: reg2.Id, + Expires: timestamppb.New(expires), + Names: []string{"example.com"}, + V2Authorizations: []int64{authzID}, + } + + // Create the order + orderNoName, err := sa.NewOrderAndAuthzs(context.Background(), &sapb.NewOrderAndAuthzsRequest{ + NewOrder: &sapb.NewOrderRequest{ + RegistrationID: inputOrderNoName.RegistrationID, + Expires: inputOrderNoName.Expires, + Names: inputOrderNoName.Names, + V2Authorizations: inputOrderNoName.V2Authorizations, + CertificateProfileName: inputOrderNoName.CertificateProfileName, + }, + }) + test.AssertNotError(t, err, "sa.NewOrderAndAuthzs failed") + + // The Order from GetOrder should match the following expected order + created = sa.clk.Now() + expectedOrderNoName := &corepb.Order{ + // The registration ID, authorizations, expiry, and names should match the + // input to NewOrderAndAuthzs + RegistrationID: inputOrderNoName.RegistrationID, + V2Authorizations: inputOrderNoName.V2Authorizations, + Names: inputOrderNoName.Names, + Expires: inputOrderNoName.Expires, + // The ID should have been set to 2 by the SA + Id: 2, + // The status should be pending + Status: string(core.StatusPending), + // The serial should be empty since this is a pending order + CertificateSerial: "", + // We should not be processing it + BeganProcessing: false, + // The created timestamp should have been set to the current time + Created: timestamppb.New(created), + } + + // Fetch the order by its ID and make sure it matches the expected + storedOrderNoName, err := sa.GetOrder(context.Background(), &sapb.OrderRequest{Id: orderNoName.Id}) + test.AssertNotError(t, err, "sa.GetOrder failed") + test.AssertDeepEquals(t, storedOrderNoName, expectedOrderNoName) +} + // TestGetAuthorization2NoRows ensures that the GetAuthorization2 function returns // the correct error when there are no results for the provided ID. func TestGetAuthorization2NoRows(t *testing.T) { diff --git a/sa/saro.go b/sa/saro.go index 4ab2d581301..b162bdb25f7 100644 --- a/sa/saro.go +++ b/sa/saro.go @@ -21,6 +21,7 @@ import ( corepb "github.com/letsencrypt/boulder/core/proto" "github.com/letsencrypt/boulder/db" berrors "github.com/letsencrypt/boulder/errors" + "github.com/letsencrypt/boulder/features" bgrpc "github.com/letsencrypt/boulder/grpc" "github.com/letsencrypt/boulder/identifier" blog "github.com/letsencrypt/boulder/log" @@ -697,7 +698,13 @@ func (ssa *SQLStorageAuthorityRO) GetOrder(ctx context.Context, req *sapb.OrderR } txn := func(tx db.Executor) (interface{}, error) { - omObj, err := tx.Get(ctx, orderModel{}, req.Id) + var omObj interface{} + var err error + if features.Get().MultipleCertificateProfiles { + omObj, err = tx.Get(ctx, orderModelv2{}, req.Id) + } else { + omObj, err = tx.Get(ctx, orderModelv1{}, req.Id) + } if err != nil { if db.IsNoRows(err) { return nil, berrors.NotFoundError("no order found for ID %d", req.Id) @@ -708,7 +715,12 @@ func (ssa *SQLStorageAuthorityRO) GetOrder(ctx context.Context, req *sapb.OrderR return nil, berrors.NotFoundError("no order found for ID %d", req.Id) } - order, err := modelToOrder(omObj.(*orderModel)) + var order *corepb.Order + if features.Get().MultipleCertificateProfiles { + order, err = modelToOrderv2(omObj.(*orderModelv2)) + } else { + order, err = modelToOrderv1(omObj.(*orderModelv1)) + } if err != nil { return nil, err } diff --git a/test/config-next/sa.json b/test/config-next/sa.json index 3e219ed4d90..45ec3810099 100644 --- a/test/config-next/sa.json +++ b/test/config-next/sa.json @@ -48,6 +48,7 @@ }, "healthCheckInterval": "4s", "features": { + "MultipleCertificateProfiles": true, "TrackReplacementCertificatesARI": true } },