Skip to content

Commit

Permalink
Follow up on 2163 (#2164)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelbrm authored Oct 27, 2023
1 parent 0788e9e commit d7414a1
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 71 deletions.
30 changes: 13 additions & 17 deletions services/skus/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,30 +539,26 @@ type CreateOrderCredsRequest struct {
BlindedCreds []string `json:"blindedCreds" valid:"base64"`
}

// CreateOrderCreds is the handler for creating order credentials
func CreateOrderCreds(service *Service) handlers.AppHandler {
// CreateOrderCreds handles requests for creating credentials.
func CreateOrderCreds(svc *Service) handlers.AppHandler {
return func(w http.ResponseWriter, r *http.Request) *handlers.AppError {
var (
req = new(CreateOrderCredsRequest)
ctx = r.Context()
logger = logging.Logger(ctx, "skus.CreateOrderCreds")
)
ctx := r.Context()
lg := logging.Logger(ctx, "skus.CreateOrderCreds")

err := requestutils.ReadJSON(ctx, r.Body, req)
if err != nil {
logger.Error().Err(err).Msg("failed to read body payload")
req := &CreateOrderCredsRequest{}
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)
}

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

var orderID = new(inputs.ID)
orderID := &inputs.ID{}
if err := inputs.DecodeAndValidateString(ctx, orderID, chi.URLParam(r, "orderID")); err != nil {
logger.Error().Err(err).Msg("failed to validate order id")
lg.Error().Err(err).Msg("failed to validate order id")
return handlers.ValidationError(
"Error validating request url parameter",
map[string]interface{}{
Expand All @@ -573,8 +569,8 @@ func CreateOrderCreds(service *Service) handlers.AppHandler {

requestID := uuid.NewV4()

if err := service.CreateOrderItemCredentials(ctx, *orderID.UUID(), req.ItemID, requestID, req.BlindedCreds); err != nil {
logger.Error().Err(err).Msg("failed to create the order credentials")
if err := svc.CreateOrderItemCredentials(ctx, *orderID.UUID(), req.ItemID, requestID, 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
129 changes: 82 additions & 47 deletions services/skus/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ const (
var (
ErrOrderUnpaid = errors.New("order not paid")
ErrOrderHasNoItems model.Error = "order has no items"
ErrCredsAlreadyExist = errors.New("credentials already exist")
ErrCredsAlreadyExist model.Error = "credentials already exist"

errInvalidIssuerResp model.Error = "invalid issuer response"
errInvalidNCredsSingleUse model.Error = "submitted more blinded creds than quantity of order item"
errInvalidNCredsTlv2 model.Error = "submitted more blinded creds than allowed for order"
errUnsupportedCredType model.Error = "unsupported credential type"
errItemDoesNotExist model.Error = "order item does not exist for order"
errCredsAlreadySubmitted model.Error = "credentials already submitted"

defaultExpiresAt = time.Now().Add(17532 * time.Hour) // 2 years
retryPolicy = retrypolicy.DefaultRetry
Expand Down Expand Up @@ -222,8 +225,9 @@ type TimeLimitedCreds struct {
Token string `json:"token"`
}

// CreateOrderItemCredentials creates the order credentials for the given order id using the supplied blinded credentials.
// If the order is unpaid an error ErrOrderUnpaid is returned.
// CreateOrderItemCredentials creates credentials for the given order id and item with the supplied blinded credentials.
//
// It handles only paid orders.
func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID, itemID, requestID uuid.UUID, blindedCreds []string) error {
order, err := s.Datastore.GetOrder(orderID)
if err != nil {
Expand All @@ -247,51 +251,15 @@ func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID, itemI
}

if orderItem == nil {
return errors.New("order item does not exist for order")
return errItemDoesNotExist
}

if orderItem.CredentialType == timeLimitedV2 {
// TLV2 check to see if we have credentials signed that match incoming blinded tokens
alreadySubmitted, err := s.Datastore.AreTimeLimitedV2CredsSubmitted(ctx, blindedCreds...)
if err != nil {
return fmt.Errorf("Error validating credentials exist for order item: %w", err)
}
if alreadySubmitted {
// since these are already submitted, no need to create order credentials
// return ok
if err := s.doCredentialsExist(ctx, orderItem, blindedCreds); err != nil {
if errors.Is(err, errCredsAlreadySubmitted) {
return nil
}

// check if we have signed credentials for this order item
// if there is no order and we have no creds, we can submit again
// similar to the outbox check case below, delete order creds will
// wipe out any already signed order creds
creds, err := s.Datastore.GetTimeLimitedV2OrderCredsByOrderItem(itemID)
if err != nil {
return fmt.Errorf("Error validating no credentials exist for order item: %w", err)
}
if creds != nil {
return ErrCredsAlreadyExist
}
// NOTE: this creates a possible race to submit between clients.
// multiple signing request outboxes can be created since their
// uniqueness constraint is on the request id.
// despite this, the uniqueness constraint of time_limited_v2_order_creds ensures that
// only one set of credentials is written for each order / item & time interval.
// as a result, one client will successfully unblind the credentials and
// the others will fail.
} else {
// check if we already have a signing request for this order, delete order creds will
// delete the prior signing request. this allows subscriptions to manage how many
// order creds are handed out.
signingOrderRequests, err := s.Datastore.GetSigningOrderRequestOutboxByOrderItem(ctx, itemID)
if err != nil {
return fmt.Errorf("Error validating no credentials exist for order item: %w", err)
}

if len(signingOrderRequests) > 0 {
return ErrCredsAlreadyExist
}
return err
}

if err := checkNumBlindedCreds(order, orderItem, len(blindedCreds)); err != nil {
Expand All @@ -308,7 +276,7 @@ func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID, itemI
return fmt.Errorf("error getting issuer for issuerID %s: %w", issuerID, err)
}

metadata := Metadata{
metadata := &Metadata{
ItemID: orderItem.ID,
OrderID: order.ID,
IssuerID: issuer.ID,
Expand All @@ -320,7 +288,7 @@ func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID, itemI
return fmt.Errorf("error serializing associated data: %w", err)
}

signingOrderRequest := SigningOrderRequest{
signReq := SigningOrderRequest{
RequestID: requestID.String(),
Data: []SigningOrder{
{
Expand All @@ -332,14 +300,81 @@ func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID, itemI
},
}

err = s.Datastore.InsertSigningOrderRequestOutbox(ctx, requestID, order.ID, orderItem.ID, signingOrderRequest)
if err != nil {
if err := s.Datastore.InsertSigningOrderRequestOutbox(ctx, requestID, order.ID, orderItem.ID, signReq); err != nil {
return fmt.Errorf("error inserting signing order request outbox orderID %s: %w", order.ID, err)
}

return nil
}

func (s *Service) doCredentialsExist(ctx context.Context, item *model.OrderItem, blindedCreds []string) error {
switch item.CredentialType {
case timeLimitedV2:
// NOTE: This creates a possible race to submit between clients.
// Multiple signing request outboxes can be created since their
// uniqueness constraint is on the request id.
// Despite this, the uniqueness constraint of time_limited_v2_order_creds ensures that
// only one set of credentials is written for each order / item & time interval.
// As a result, one client will successfully unblind the credentials and
// the others will fail.

return s.doTLV2Exist(ctx, item, blindedCreds)
default:
return s.doCredsExist(ctx, item)
}
}

func (s *Service) doTLV2Exist(ctx context.Context, item *model.OrderItem, blindedCreds []string) error {
if item.CredentialType != timeLimitedV2 {
return errUnsupportedCredType
}

// Check TLV2 to see if we have credentials signed that match incoming blinded tokens.
alreadySubmitted, err := s.Datastore.AreTimeLimitedV2CredsSubmitted(ctx, blindedCreds...)
if err != nil {
return fmt.Errorf("error validating credentials exist for order item: %w", err)
}

if alreadySubmitted {
// No need to create order credentials, since these are already submitted.
return errCredsAlreadySubmitted
}

// Check if we have signed credentials for this order item.
// If there is no order and no creds, we can submit again.
// Similar to the outbox check case, delete order creds will wipe out any already signed order creds.
creds, err := s.Datastore.GetTimeLimitedV2OrderCredsByOrderItem(item.ID)
if err != nil {
return fmt.Errorf("error validating no credentials exist for order item: %w", err)
}

if creds != nil {
return ErrCredsAlreadyExist
}

return nil
}

func (s *Service) doCredsExist(ctx context.Context, item *model.OrderItem) error {
if item.CredentialType == timeLimitedV2 {
return errUnsupportedCredType
}

// Check if we already have a signing request for this order, delete order creds will
// delete the prior signing request.
// This allows subscriptions to manage how many order creds are handed out.
signingOrderRequests, err := s.Datastore.GetSigningOrderRequestOutboxByOrderItem(ctx, item.ID)
if err != nil {
return fmt.Errorf("error validating no credentials exist for order item: %w", err)
}

if len(signingOrderRequests) > 0 {
return ErrCredsAlreadyExist
}

return nil
}

// OrderWorker attempts to work on an order job by signing the blinded credentials of the client
type OrderWorker interface {
SignOrderCreds(ctx context.Context, orderID uuid.UUID, issuer Issuer, blindedCreds []string) (*OrderCreds, error)
Expand Down
11 changes: 4 additions & 7 deletions services/skus/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -1118,18 +1118,15 @@ func (pg *Postgres) UpdateSigningOrderRequestOutboxTx(ctx context.Context, tx *s
return nil
}

// InsertSigningOrderRequestOutbox insert the signing order request into the outbox.
func (pg *Postgres) InsertSigningOrderRequestOutbox(ctx context.Context, requestID uuid.UUID, orderID uuid.UUID,
itemID uuid.UUID, signingOrderRequest SigningOrderRequest) error {

// InsertSigningOrderRequestOutbox inserts the signing order request into the outbox.
func (pg *Postgres) InsertSigningOrderRequestOutbox(ctx context.Context, requestID, orderID, itemID uuid.UUID, signingOrderRequest SigningOrderRequest) error {
message, err := json.Marshal(signingOrderRequest)
if err != nil {
return fmt.Errorf("error marshalling signing order request: %w", err)
}

_, err = pg.ExecContext(ctx, `insert into signing_order_request_outbox(request_id, order_id, item_id, message_data)
values ($1, $2, $3, $4)`, requestID, orderID, itemID, message)
if err != nil {
const q = `INSERT INTO signing_order_request_outbox (request_id, order_id, item_id, message_data) VALUES ($1, $2, $3, $4)`
if _, err := pg.ExecContext(ctx, q, requestID, orderID, itemID, message); err != nil {
return fmt.Errorf("error inserting order request outbox row: %w", err)
}

Expand Down

0 comments on commit d7414a1

Please sign in to comment.