Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

production 2023-12-18_1 #2256

Merged
merged 1 commit into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 115 additions & 39 deletions services/skus/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,17 @@ func Router(

r.Route("/{orderID}/credentials", func(cr chi.Router) {
cr.Use(NewCORSMwr(copts, http.MethodGet, http.MethodPost))
cr.Method(http.MethodPost, "/", metricsMwr("CreateOrderCreds", CreateOrderCreds(svc)))
cr.Method(http.MethodGet, "/", metricsMwr("GetOrderCreds", GetOrderCreds(svc)))
cr.Method(http.MethodGet, "/{itemID}", metricsMwr("GetOrderCredsByID", GetOrderCredsByID(svc)))
cr.Method(http.MethodPost, "/", metricsMwr("CreateOrderCreds", CreateOrderCreds(svc)))
cr.Method(http.MethodDelete, "/", metricsMwr("DeleteOrderCreds", authMwr(DeleteOrderCreds(svc))))

// Handle the old endpoint while the new is being rolled out:
// - true: the handler uses itemID as the request id, which is the old mode;
// - false: the handler uses the requestID from the URI.
cr.Method(http.MethodGet, "/{itemID}", metricsMwr("GetOrderCredsByID", getOrderCredsByID(svc, true)))
cr.Method(http.MethodGet, "/items/{itemID}/batches/{requestID}", metricsMwr("GetOrderCredsByID", getOrderCredsByID(svc, false)))

cr.Method(http.MethodPut, "/items/{itemID}/batches/{requestID}", metricsMwr("CreateOrderItemCreds", createItemCreds(svc)))
})

return r
Expand Down Expand Up @@ -535,7 +542,7 @@ func CreateAnonCardTransaction(service *Service) handlers.AppHandler {
})
}

// CreateOrderCredsRequest includes the item ID and blinded credentials which to be signed
// CreateOrderCredsRequest includes the item ID and blinded credentials which to be signed.
type CreateOrderCredsRequest struct {
ItemID uuid.UUID `json:"itemId" valid:"-"`
BlindedCreds []string `json:"blindedCreds" valid:"base64"`
Expand Down Expand Up @@ -569,9 +576,65 @@ func CreateOrderCreds(svc *Service) handlers.AppHandler {
)
}

requestID := uuid.NewV4()
// Use the itemID for the request id so the old credential uniqueness constraint remains enforced.
reqID := req.ItemID

if err := svc.CreateOrderItemCredentials(ctx, *orderID.UUID(), req.ItemID, reqID, req.BlindedCreds); err != nil {
lg.Error().Err(err).Msg("failed to create the order credentials")
return handlers.WrapError(err, "Error creating order creds", http.StatusBadRequest)
}

return handlers.RenderContent(ctx, nil, w, http.StatusOK)
}
}

// createItemCredsRequest includes the blinded credentials to be signed.
type createItemCredsRequest struct {
BlindedCreds []string `json:"blindedCreds" valid:"base64"`
}

// createItemCreds handles requests for creating credentials for an item.
func createItemCreds(svc *Service) handlers.AppHandler {
return func(w http.ResponseWriter, r *http.Request) *handlers.AppError {
ctx := r.Context()
lg := logging.Logger(ctx, "skus.createItemCreds")

req := &createItemCredsRequest{}
if err := requestutils.ReadJSON(ctx, r.Body, req); err != nil {
lg.Error().Err(err).Msg("failed to read body payload")
return handlers.WrapError(err, "Error in request body", http.StatusBadRequest)
}

if _, err := govalidator.ValidateStruct(req); err != nil {
lg.Error().Err(err).Msg("failed to validate struct")
return handlers.WrapValidationError(err)
}

orderID := &inputs.ID{}
if err := inputs.DecodeAndValidateString(ctx, orderID, chi.URLParamFromCtx(ctx, "orderID")); err != nil {
lg.Error().Err(err).Msg("failed to validate order id")
return handlers.ValidationError("Error validating request url parameter", map[string]interface{}{
"orderID": err.Error(),
})
}

itemID := &inputs.ID{}
if err := inputs.DecodeAndValidateString(ctx, itemID, chi.URLParamFromCtx(ctx, "itemID")); err != nil {
lg.Error().Err(err).Msg("failed to validate item id")
return handlers.ValidationError("Error validating request url parameter", map[string]interface{}{
"itemID": err.Error(),
})
}

reqID := &inputs.ID{}
if err := inputs.DecodeAndValidateString(ctx, reqID, chi.URLParamFromCtx(ctx, "requestID")); err != nil {
lg.Error().Err(err).Msg("failed to validate request id")
return handlers.ValidationError("Error validating request url parameter", map[string]interface{}{
"requestID": err.Error(),
})
}

if err := svc.CreateOrderItemCredentials(ctx, *orderID.UUID(), req.ItemID, requestID, req.BlindedCreds); err != nil {
if err := svc.CreateOrderItemCredentials(ctx, *orderID.UUID(), *itemID.UUID(), *reqID.UUID(), req.BlindedCreds); err != nil {
lg.Error().Err(err).Msg("failed to create the order credentials")
return handlers.WrapError(err, "Error creating order creds", http.StatusBadRequest)
}
Expand Down Expand Up @@ -638,54 +701,67 @@ func DeleteOrderCreds(service *Service) handlers.AppHandler {
}
}

// GetOrderCredsByID is the handler for fetching order credentials by an item id
func GetOrderCredsByID(service *Service) handlers.AppHandler {
// getOrderCredsByID handles requests for fetching order credentials by an item id.
//
// Requests may come in via two endpoints:
// - /{itemID} – legacyMode, reqID == itemID
// - /items/{itemID}/batches/{requestID} – new mode, reqID == requestID.
//
// The legacy mode will be gone after confirming a successful rollout.
//
// TODO: Clean up the legacy mode.
func getOrderCredsByID(svc *Service, legacyMode bool) handlers.AppHandler {
return handlers.AppHandler(func(w http.ResponseWriter, r *http.Request) *handlers.AppError {
ctx := r.Context()

// get the IDs from the URL
var (
orderID = new(inputs.ID)
itemID = new(inputs.ID)
validationPayload = map[string]interface{}{}
err error
)

// decode and validate orderID url param
if err = inputs.DecodeAndValidateString(
context.Background(), orderID, chi.URLParam(r, "orderID")); err != nil {
validationPayload["orderID"] = err.Error()
orderID := &inputs.ID{}
if err := inputs.DecodeAndValidateString(ctx, orderID, chi.URLParamFromCtx(ctx, "orderID")); err != nil {
return handlers.ValidationError("Error validating request url parameter", map[string]interface{}{
"orderID": err.Error(),
})
}

// decode and validate itemID url param
if err = inputs.DecodeAndValidateString(
context.Background(), itemID, chi.URLParam(r, "itemID")); err != nil {
validationPayload["itemID"] = err.Error()
itemID := &inputs.ID{}
if err := inputs.DecodeAndValidateString(ctx, itemID, chi.URLParamFromCtx(ctx, "itemID")); err != nil {
return handlers.ValidationError("Error validating request url parameter", map[string]interface{}{
"itemID": err.Error(),
})
}

// did we get any validation errors?
if len(validationPayload) > 0 {
return handlers.ValidationError(
"Error validating request url parameter",
validationPayload)
var reqID uuid.UUID
if legacyMode {
reqID = *itemID.UUID()
} else {
reqIDRaw := &inputs.ID{}
if err := inputs.DecodeAndValidateString(ctx, reqIDRaw, chi.URLParamFromCtx(ctx, "requestID")); err != nil {
return handlers.ValidationError("Error validating request url parameter", map[string]interface{}{
"requestID": err.Error(),
})
}

reqID = *reqIDRaw.UUID()
}

creds, status, err := service.GetItemCredentials(r.Context(), *orderID.UUID(), *itemID.UUID())
creds, status, err := svc.GetItemCredentials(ctx, *orderID.UUID(), *itemID.UUID(), reqID)
if err != nil {
if errors.Is(err, errSetRetryAfter) {
// error specifies a retry after period, add to response header
avg, err := service.Datastore.GetOutboxMovAvgDurationSeconds()
if err != nil {
return handlers.WrapError(err, "Error getting credential retry-after", status)
}
w.Header().Set("Retry-After", strconv.FormatInt(avg, 10))
} else {
if !errors.Is(err, errSetRetryAfter) {
return handlers.WrapError(err, "Error getting credentials", status)
}

// Add to response header as error specifies a retry after period.
avg, err := svc.Datastore.GetOutboxMovAvgDurationSeconds()
if err != nil {
return handlers.WrapError(err, "Error getting credential retry-after", status)
}

w.Header().Set("Retry-After", strconv.FormatInt(avg, 10))
}

if creds == nil {
return handlers.RenderContent(r.Context(), map[string]interface{}{}, w, status)
return handlers.RenderContent(ctx, map[string]interface{}{}, w, status)
}
return handlers.RenderContent(r.Context(), creds, w, status)

return handlers.RenderContent(ctx, creds, w, status)
})
}

Expand Down
2 changes: 1 addition & 1 deletion services/skus/controllers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1435,7 +1435,7 @@ func (suite *ControllersTestSuite) TestExpiredTimeLimitedCred() {
ValidFor: &valid,
}

creds, status, err := suite.service.GetTimeLimitedCreds(ctx, order)
creds, status, err := suite.service.GetTimeLimitedCreds(ctx, order, uuid.Nil, uuid.Nil)
suite.Require().True(creds == nil, "should not get creds back")
suite.Require().True(status == http.StatusBadRequest, "should not get creds back")
suite.Require().Error(err, "should get an error")
Expand Down
2 changes: 1 addition & 1 deletion services/skus/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ func (s *SignedOrderCredentialsHandler) Handle(ctx context.Context, message kafk
defer rollback()

// Check to see if the signing request has not been deleted whilst signing the request.
sor, err := s.datastore.GetSigningOrderRequestOutboxByRequestIDTx(ctx, tx, requestID)
sor, err := s.datastore.GetSigningOrderRequestOutboxByRequestID(ctx, tx, requestID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("error get signing order credentials tx: %w", err)
}
Expand Down
63 changes: 41 additions & 22 deletions services/skus/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import (
const (
signingRequestBatchSize = 10

errNotFound = model.Error("not found")
errNotFound = model.Error("not found")
errNoTLV2Creds = model.Error("no unexpired time-limited-v2 credentials found")
)

// Datastore abstracts over the underlying datastore.
Expand Down Expand Up @@ -81,11 +82,12 @@ type Datastore interface {
InsertSignedOrderCredentialsTx(ctx context.Context, tx *sqlx.Tx, signedOrderResult *SigningOrderResult) error
AreTimeLimitedV2CredsSubmitted(ctx context.Context, blindedCreds ...string) (bool, error)
GetTimeLimitedV2OrderCredsByOrder(orderID uuid.UUID) (*TimeLimitedV2Creds, error)
GetTLV2Creds(ctx context.Context, dbi sqlx.QueryerContext, ordID, itemID, reqID uuid.UUID) (*TimeLimitedV2Creds, error)
DeleteTimeLimitedV2OrderCredsByOrderTx(ctx context.Context, tx *sqlx.Tx, orderID uuid.UUID) error
GetTimeLimitedV2OrderCredsByOrderItem(itemID uuid.UUID) (*TimeLimitedV2Creds, error)
InsertTimeLimitedV2OrderCredsTx(ctx context.Context, tx *sqlx.Tx, tlv2 TimeAwareSubIssuedCreds) error
InsertSigningOrderRequestOutbox(ctx context.Context, requestID uuid.UUID, orderID uuid.UUID, itemID uuid.UUID, signingOrderRequest SigningOrderRequest) error
GetSigningOrderRequestOutboxByRequestIDTx(ctx context.Context, tx *sqlx.Tx, requestID uuid.UUID) (*SigningOrderRequestOutbox, error)
GetSigningOrderRequestOutboxByRequestID(ctx context.Context, dbi sqlx.QueryerContext, reqID uuid.UUID) (*SigningOrderRequestOutbox, error)
GetSigningOrderRequestOutboxByOrder(ctx context.Context, orderID uuid.UUID) ([]SigningOrderRequestOutbox, error)
GetSigningOrderRequestOutboxByOrderItem(ctx context.Context, itemID uuid.UUID) ([]SigningOrderRequestOutbox, error)
DeleteSigningOrderRequestOutboxByOrderTx(ctx context.Context, tx *sqlx.Tx, orderID uuid.UUID) error
Expand Down Expand Up @@ -996,6 +998,34 @@ func (pg *Postgres) GetTimeLimitedV2OrderCredsByOrder(orderID uuid.UUID) (*TimeL
return &timeLimitedV2Creds, nil
}

// GetTLV2Creds returns all the non expired tlv2 credentials for a given order, item and request ids.
//
// If no credentials have been found, the method returns errNoTLV2Creds.
func (pg *Postgres) GetTLV2Creds(ctx context.Context, dbi sqlx.QueryerContext, ordID, itemID, reqID uuid.UUID) (*TimeLimitedV2Creds, error) {
const q = `SELECT
order_id, item_id, issuer_id, blinded_creds, signed_creds,
batch_proof, public_key, valid_from, valid_to
FROM time_limited_v2_order_creds
WHERE order_id = $1 AND item_id = $2 AND request_id = $3 AND valid_to > now()`

creds := make([]TimeAwareSubIssuedCreds, 0)
if err := sqlx.SelectContext(ctx, dbi, &creds, q, ordID, itemID, reqID); err != nil {
return nil, err
}

if len(creds) == 0 {
return nil, errNoTLV2Creds
}

result := &TimeLimitedV2Creds{
OrderID: creds[0].OrderID,
IssuerID: creds[0].IssuerID,
Credentials: creds,
}

return result, nil
}

// GetTimeLimitedV2OrderCredsByOrderItem returns all the order credentials for a single order item.
func (pg *Postgres) GetTimeLimitedV2OrderCredsByOrderItem(itemID uuid.UUID) (*TimeLimitedV2Creds, error) {
query := `
Expand Down Expand Up @@ -1083,29 +1113,19 @@ func (pg *Postgres) GetSigningOrderRequestOutboxByOrderItem(ctx context.Context,
}

// GetSigningOrderRequestOutboxByRequestID retrieves the SigningOrderRequestOutbox by requestID.
//
// An error is returned if the result set is empty.
func (pg *Postgres) GetSigningOrderRequestOutboxByRequestID(ctx context.Context, requestID uuid.UUID) (*SigningOrderRequestOutbox, error) {
var signingRequestOutbox SigningOrderRequestOutbox
err := pg.RawDB().GetContext(ctx, &signingRequestOutbox,
`select request_id, order_id, item_id, completed_at, message_data
from signing_order_request_outbox where request_id = $1`, requestID)
if err != nil {
return nil, fmt.Errorf("error retrieving signing request from outbox: %w", err)
}
return &signingRequestOutbox, nil
}
func (pg *Postgres) GetSigningOrderRequestOutboxByRequestID(ctx context.Context, dbi sqlx.QueryerContext, reqID uuid.UUID) (*SigningOrderRequestOutbox, error) {
const q = `SELECT request_id, order_id, item_id, completed_at, message_data
FROM signing_order_request_outbox
WHERE request_id = $1 FOR UPDATE`

// GetSigningOrderRequestOutboxByRequestIDTx retrieves the SigningOrderRequestOutbox by requestID.
// An error is returned if the result set is empty.
func (pg *Postgres) GetSigningOrderRequestOutboxByRequestIDTx(ctx context.Context, tx *sqlx.Tx, requestID uuid.UUID) (*SigningOrderRequestOutbox, error) {
var signingRequestOutbox SigningOrderRequestOutbox
err := tx.GetContext(ctx, &signingRequestOutbox,
`select request_id, order_id, item_id, completed_at, message_data
from signing_order_request_outbox where request_id = $1 for update`, requestID)
if err != nil {
result := &SigningOrderRequestOutbox{}
if err := sqlx.GetContext(ctx, dbi, result, q, reqID); err != nil {
return nil, fmt.Errorf("error retrieving signing request from outbox: %w", err)
}
return &signingRequestOutbox, nil

return result, nil
}

// UpdateSigningOrderRequestOutboxTx updates a signing order request outbox message for the given requestID.
Expand Down Expand Up @@ -1277,7 +1297,6 @@ func (pg *Postgres) InsertSignedOrderCredentialsTx(ctx context.Context, tx *sqlx
}

case timeLimitedV2:

if so.ValidTo.Value() == nil {
return fmt.Errorf("error validTo for order creds orderID %s itemID %s is null: %w",
metadata.OrderID, metadata.ItemID, err)
Expand Down
22 changes: 18 additions & 4 deletions services/skus/instrumented_datastore.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading