mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 02:02:43 +00:00 
			
		
		
		
	Implement partial_failure_response_code_override for batch requests (#17118)
* Implement partial_failure_response_code_override for batch requests * docs * changelog * one more test case
This commit is contained in:
		| @@ -4,8 +4,6 @@ import ( | ||||
| 	"context" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/sdk/framework" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/errutil" | ||||
| 	"github.com/hashicorp/vault/sdk/helper/keysutil" | ||||
| @@ -51,6 +49,14 @@ Base64 encoded nonce value used during encryption. Must be provided if | ||||
| convergent encryption is enabled for this key and the key was generated with | ||||
| Vault 0.6.1. Not required for keys created in 0.6.2+.`, | ||||
| 			}, | ||||
| 			"partial_failure_response_code": { | ||||
| 				Type: framework.TypeInt, | ||||
| 				Description: ` | ||||
| Ordinarily, if a batch item fails to decrypt due to a bad input, but other batch items succeed,  | ||||
| the HTTP response code is 400 (Bad Request).  Some applications may want to treat partial failures differently. | ||||
| Providing the parameter returns the given response code integer instead of a 400 in this case.  If all values fail | ||||
| HTTP 400 is still returned.`, | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| @@ -142,6 +148,7 @@ func (b *backend) pathDecryptWrite(ctx context.Context, req *logical.Request, d | ||||
| 		p.Lock(false) | ||||
| 	} | ||||
|  | ||||
| 	successesInBatch := false | ||||
| 	for i, item := range batchInputItems { | ||||
| 		if batchResponseItems[i].Error != "" { | ||||
| 			continue | ||||
| @@ -158,6 +165,7 @@ func (b *backend) pathDecryptWrite(ctx context.Context, req *logical.Request, d | ||||
| 			batchResponseItems[i].Error = err.Error() | ||||
| 			continue | ||||
| 		} | ||||
| 		successesInBatch = true | ||||
| 		batchResponseItems[i].Plaintext = plaintext | ||||
| 	} | ||||
|  | ||||
| @@ -183,18 +191,7 @@ func (b *backend) pathDecryptWrite(ctx context.Context, req *logical.Request, d | ||||
|  | ||||
| 	p.Unlock() | ||||
|  | ||||
| 	// Depending on the errors in the batch, different status codes should be returned. User errors | ||||
| 	// will return a 400 and precede internal errors which return a 500. The reasoning behind this is | ||||
| 	// that user errors are non-retryable without making changes to the request, and should be surfaced | ||||
| 	// to the user first. | ||||
| 	switch { | ||||
| 	case userErrorInBatch: | ||||
| 		return logical.RespondWithStatusCode(resp, req, http.StatusBadRequest) | ||||
| 	case internalErrorInBatch: | ||||
| 		return logical.RespondWithStatusCode(resp, req, http.StatusInternalServerError) | ||||
| 	} | ||||
|  | ||||
| 	return resp, nil | ||||
| 	return batchRequestResponse(d, resp, req, successesInBatch, userErrorInBatch, internalErrorInBatch) | ||||
| } | ||||
|  | ||||
| const pathDecryptHelpSyn = `Decrypt a ciphertext value using a named key` | ||||
|   | ||||
| @@ -119,6 +119,7 @@ func TestTransit_BatchDecryption_DerivedKey(t *testing.T) { | ||||
| 		want           []DecryptBatchResponseItem | ||||
| 		shouldErr      bool | ||||
| 		wantHTTPStatus int | ||||
| 		params         map[string]interface{} | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:      "nil-input", | ||||
| @@ -182,6 +183,19 @@ func TestTransit_BatchDecryption_DerivedKey(t *testing.T) { | ||||
| 			}, | ||||
| 			wantHTTPStatus: http.StatusBadRequest, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "batch-partial-success-overridden-response", | ||||
| 			in: []interface{}{ | ||||
| 				map[string]interface{}{"ciphertext": encryptedItems[0].Ciphertext, "context": plaintextItems[1].context}, | ||||
| 				map[string]interface{}{"ciphertext": encryptedItems[1].Ciphertext, "context": plaintextItems[1].context}, | ||||
| 			}, | ||||
| 			want: []DecryptBatchResponseItem{ | ||||
| 				{Error: "cipher: message authentication failed"}, | ||||
| 				{Plaintext: plaintextItems[1].plaintext}, | ||||
| 			}, | ||||
| 			params:         map[string]interface{}{"partial_failure_response_code": http.StatusAccepted}, | ||||
| 			wantHTTPStatus: http.StatusAccepted, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "batch-full-failure", | ||||
| 			in: []interface{}{ | ||||
| @@ -194,6 +208,20 @@ func TestTransit_BatchDecryption_DerivedKey(t *testing.T) { | ||||
| 			}, | ||||
| 			wantHTTPStatus: http.StatusBadRequest, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "batch-full-failure-overridden-response", | ||||
| 			in: []interface{}{ | ||||
| 				map[string]interface{}{"ciphertext": encryptedItems[0].Ciphertext, "context": plaintextItems[1].context}, | ||||
| 				map[string]interface{}{"ciphertext": encryptedItems[1].Ciphertext, "context": plaintextItems[0].context}, | ||||
| 			}, | ||||
| 			want: []DecryptBatchResponseItem{ | ||||
| 				{Error: "cipher: message authentication failed"}, | ||||
| 				{Error: "cipher: message authentication failed"}, | ||||
| 			}, | ||||
| 			params: map[string]interface{}{"partial_failure_response_code": http.StatusAccepted}, | ||||
| 			// Full failure, shouldn't affect status code | ||||
| 			wantHTTPStatus: http.StatusBadRequest, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| @@ -206,6 +234,9 @@ func TestTransit_BatchDecryption_DerivedKey(t *testing.T) { | ||||
| 					"batch_input": tt.in, | ||||
| 				}, | ||||
| 			} | ||||
| 			for k, v := range tt.params { | ||||
| 				req.Data[k] = v | ||||
| 			} | ||||
| 			resp, err = b.HandleRequest(context.Background(), req) | ||||
|  | ||||
| 			didErr := err != nil || (resp != nil && resp.IsError()) | ||||
|   | ||||
| @@ -113,6 +113,14 @@ will severely impact the ciphertext's security.`, | ||||
| Must be 0 (for latest) or a value greater than or equal | ||||
| to the min_encryption_version configured on the key.`, | ||||
| 			}, | ||||
| 			"partial_failure_response_code": { | ||||
| 				Type: framework.TypeInt, | ||||
| 				Description: ` | ||||
| Ordinarily, if a batch item fails to encrypt due to a bad input, but other batch items succeed,  | ||||
| the HTTP response code is 400 (Bad Request).  Some applications may want to treat partial failures differently. | ||||
| Providing the parameter returns the given response code integer instead of a 400 in this case. If all values fail | ||||
| HTTP 400 is still returned.`, | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| @@ -375,6 +383,7 @@ func (b *backend) pathEncryptWrite(ctx context.Context, req *logical.Request, d | ||||
| 	// item fails, respectively mark the error in the response | ||||
| 	// collection and continue to process other items. | ||||
| 	warnAboutNonceUsage := false | ||||
| 	successesInBatch := false | ||||
| 	for i, item := range batchInputItems { | ||||
| 		if batchResponseItems[i].Error != "" { | ||||
| 			continue | ||||
| @@ -402,6 +411,7 @@ func (b *backend) pathEncryptWrite(ctx context.Context, req *logical.Request, d | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		successesInBatch = true | ||||
| 		keyVersion := item.KeyVersion | ||||
| 		if keyVersion == 0 { | ||||
| 			keyVersion = p.LatestVersion | ||||
| @@ -443,13 +453,27 @@ func (b *backend) pathEncryptWrite(ctx context.Context, req *logical.Request, d | ||||
|  | ||||
| 	p.Unlock() | ||||
|  | ||||
| 	// Depending on the errors in the batch, different status codes should be returned. User errors | ||||
| 	// will return a 400 and precede internal errors which return a 500. The reasoning behind this is | ||||
| 	// that user errors are non-retryable without making changes to the request, and should be surfaced | ||||
| 	// to the user first. | ||||
| 	return batchRequestResponse(d, resp, req, successesInBatch, userErrorInBatch, internalErrorInBatch) | ||||
| } | ||||
|  | ||||
| // Depending on the errors in the batch, different status codes should be returned. User errors | ||||
| // will return a 400 and precede internal errors which return a 500. The reasoning behind this is | ||||
| // that user errors are non-retryable without making changes to the request, and should be surfaced | ||||
| // to the user first. | ||||
| func batchRequestResponse(d *framework.FieldData, resp *logical.Response, req *logical.Request, successesInBatch, userErrorInBatch, internalErrorInBatch bool) (*logical.Response, error) { | ||||
| 	switch { | ||||
| 	case userErrorInBatch: | ||||
| 		return logical.RespondWithStatusCode(resp, req, http.StatusBadRequest) | ||||
| 		code := http.StatusBadRequest | ||||
| 		if successesInBatch { | ||||
| 			if codeRaw, ok := d.GetOk("partial_failure_response_code"); ok { | ||||
| 				code = codeRaw.(int) | ||||
| 				if code < 1 || code > 599 { | ||||
| 					resp.AddWarning("invalid HTTP response code override from partial_failure_response_code, reverting to HTTP 400") | ||||
| 					code = http.StatusBadRequest | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		return logical.RespondWithStatusCode(resp, req, code) | ||||
| 	case internalErrorInBatch: | ||||
| 		return logical.RespondWithStatusCode(resp, req, http.StatusInternalServerError) | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										4
									
								
								changelog/17118.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								changelog/17118.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| ```release-note:improvement | ||||
| secrets/transit: Added a parameter to encrypt/decrypt batch operations to allow the caller to | ||||
| override the HTTP response code in case of partial user-input failures. | ||||
| ``` | ||||
| @@ -579,6 +579,12 @@ will be returned. | ||||
|   all nonces are unique for a given context. Failing to do so will severely | ||||
|   impact the ciphertext's security. | ||||
|  | ||||
| - `partial_failure_response_code` `(int: 400)` Ordinarily, if a batch item fails  | ||||
| to encrypt due to a bad input, but other batch items succeed, the HTTP response  | ||||
| code is 400 (Bad Request).  Some applications may want to treat partial failures | ||||
| differently.  Providing the parameter returns the given response code integer  | ||||
| instead of a 400 in this case. If all values fail HTTP 400 is still returned. | ||||
|  | ||||
| ~>**NOTE:** All plaintext data **must be base64-encoded**. The reason for this | ||||
| requirement is that Vault does not require that the plaintext is "text". It | ||||
| could be a binary file such as a PDF or image. The easiest safe transport | ||||
| @@ -663,6 +669,11 @@ This endpoint decrypts the provided ciphertext using the named key. | ||||
|     } | ||||
|   ] | ||||
|   ``` | ||||
| - `partial_failure_response_code` `(int: 400)` Ordinarily, if a batch item fails  | ||||
| to encrypt due to a bad input, but other batch items succeed, the HTTP response  | ||||
| code is 400 (Bad Request).  Some applications may want to treat partial failures | ||||
| differently.  Providing the parameter returns the given response code integer  | ||||
| instead of a 400 in this case. If all values fail HTTP 400 is still returned. | ||||
|  | ||||
| ### Sample Payload | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Scott Miller
					Scott Miller