Skip to content

Commit

Permalink
Add Endpoints for Submitting and Fetching Credentials Based on Reques…
Browse files Browse the repository at this point in the history
…t ID (#2183)

* add create order item credentials which takes request id

* add endpoint for getting credentials by request id

* Rearrange code in CreateOrderCreds and fix compilation issues

* Tidy up and refactor getOrderCredsByID

* Tidy up and refactor getOrderCredsByID further

* Clean up further GetSigningOrderRequestOutboxByRequestID

* Delete generated garbage

* Fix broken tests

* Undo unrelated changes

* Pass context and database interface to new Datastore method and rename it

* Refactor new Datastore method and rename it to GetTLV2Creds

* Tidy up GetSingleUseCreds

* Tidy up GetSingleUseCreds further

* Undo unrelated changes

* Handle happy path early

* Tidy up GetTimeLimitedCreds

* Dont pass entire order where id is enough

* Clean up GetCredentials and GetItemCredentials

* Add tests for HasItem

---------

Co-authored-by: PavelBrm <[email protected]>
  • Loading branch information
evq and pavelbrm authored Dec 18, 2023
1 parent cf9c238 commit f188989
Show file tree
Hide file tree
Showing 9 changed files with 456 additions and 187 deletions.
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

0 comments on commit f188989

Please sign in to comment.