Add route for errors with secrets and PII (#354)

* Add route for errors with secrets and PII

Relates to Azure/azure-sdk#3959

* Resolve feedback

* Run prettier

* Resolve PR feedback

* Bump patch version

* Add POST endpoint

Resolves feedback for more routes.

* Resolve PR feedback
This commit is contained in:
Heath Stewart 2022-03-01 13:15:11 -08:00 коммит произвёл GitHub
Родитель 3e868f255b
Коммит fb790237dc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 312 добавлений и 5 удалений

Просмотреть файл

@ -1,9 +1,10 @@
# Writing mock apis
1. First step is to create a new file typescript file in the [src/test-routes](../src/test-routes) folder:
1. All the needed imports are from the `api` folder
1. Define the category for your apis using `app.category("vanilla" | "azure" | "optional", () => {})`
1. Start writing mock apis inside the `category` callback
1. First step is to create a new file typescript file in the [src/test-routes](../src/test-routes) folder.
1. All the needed imports are from the `api` folder.
1. Define the category for your apis using `app.category("vanilla" | "azure" | "optional", () => {})`.
1. Start writing mock apis inside the `category` callback as shown below.
1. Add a swagger file e.g., using the same file name, to the [swagger](../swagger) folder.
## Example
@ -33,6 +34,77 @@ app.category("vanilla", () => {
});
```
### Example swagger
```json
{
"swagger": "2.0",
"info": {
"title": "Test Client",
"description": "Client for an example test service.",
"version": "1.0.0"
},
"host": "localhost:3000",
"schemes": ["http"],
"produces": ["application/json"],
"paths": {
"/test": {
"get": {
"operationId": "GetMyTest",
"description": "Gets a test object.",
"responses": {
"200": {
"description": "A test object containing 'foo' and 'bar'.",
"schema": { "$ref": "#/definitions/MyTest" }
}
}
},
"post": {
"operationId": "PostMyTest",
"description": "Creates a test object..",
"parameters": [
{
"name": "input",
"description": "A test object containing 'foo' and 'bar'.",
"in": "body",
"required": true,
"schema": { "$ref": "#/definitions/MyTest" }
}
],
"responses": {
"200": {
"description": "A response indicating success.",
"schema": { "$ref": "#/definitions/PostMyTestResponse" }
}
}
}
}
},
"definitions": {
"MyTest": {
"type": "object",
"properties": {
"foo": {
"type": "string"
},
"bar": {
"type": "string"
}
},
"required": ["foo", "bar"]
},
"PostMyTestResponse": {
"type": "object",
"properties": {
"succeeded": {
"type": "boolean"
}
}
}
}
}
```
## How to build response
Return the reponse object. [See type](../src/api/mock-response.ts)

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "@microsoft.azure/autorest.testserver",
"version": "3.3.17",
"version": "3.3.18",
"description": "Autorest test server.",
"main": "dist/cli/cli.js",
"bin": {

Просмотреть файл

@ -8,6 +8,7 @@ import {
validateBodyNotEmpty,
validateXMLBodyEquals,
validateHeader,
validateQueryParam,
} from "./request-validations";
/**
@ -68,6 +69,15 @@ export class RequestExpectation {
validateHeader(this.originalRequest, headerName, expectedValue);
}
/**
* Expect the query string of the request contains the expected name/value pair.
* @param paramName Name of the query parameter.
* @param expectedValue Value expected of the query parameter.
*/
public containsQueryParam(paramName: string, expectedValue: string): void {
validateQueryParam(this.originalRequest, paramName, expectedValue);
}
/**
* Check if two requests are equal
* @param actual Actual value

Просмотреть файл

@ -95,3 +95,13 @@ export const validateHeader = (request: RequestExt, headerName: string, expected
throw new ValidationError(`Expected ${expected} but got ${actual}`, expected, actual);
}
};
/**
* Check whether the query string contains the given parameter name and value.
*/
export const validateQueryParam = (request: RequestExt, paramName: string, expected: string): void => {
const actual = request.query[paramName];
if (actual !== expected) {
throw new ValidationError(`Expected query param ${paramName}=${expected} but got ${actual}`, expected, actual);
}
};

Просмотреть файл

@ -0,0 +1,55 @@
import { app, json } from "../api";
app.category("vanilla", () => {
// Returns an error response with secrets and PII in the headers and body.
app.get("/secrets/error", "ErrorWithSecrets", (req) => {
return {
status: 403,
headers: {
// Following headers should be redacted.
"x-ms-pii": "true",
// Following headers should not be redacted.
"x-ms-request-id": "5e123516-834e-4222-9e80-353108d33357",
"x-ms-version": "2022-02-01",
},
body: json({
error: {
code: "Unauthorized",
message: "The user 'user@contoso.com' is unauthorized.",
details: [
{
code: "UnauthorizedSharedKey",
innererror: "Shared key 1c88a67921784300a462b2cb61da2339 is not permitted access.",
},
],
token: "1c88a67921784300a462b2cb61da2339",
},
primaryKey: "1c88a67921784300a462b2cb61da2339",
connectionString: "Key1=1c88a67921784300a462b2cb61da2339",
}),
};
});
app.post("/secrets/[:]create", "RequestWithSecrets", (req) => {
req.expect.containsHeader("authorization", "SharedKey 1c88a67921784300a462b2cb61da2339");
req.expect.containsQueryParam("key", "1c88a67921784300a462b2cb61da2339");
req.expect.bodyEquals({ key: "1c88a67921784300a462b2cb61da2339" });
return {
status: 200,
headers: {
// Following headers should be redacted.
"x-ms-pii": "true",
// Following headers should not be redacted.
"x-ms-request-id": "49997f20-3cee-4c0c-92ff-572acbbed13d",
"x-ms-version": "2022-02-01",
},
body: json({
key: "1c88a67921784300a462b2cb61da2339",
value: "secret",
}),
};
});
});

Просмотреть файл

@ -0,0 +1,160 @@
{
"swagger": "2.0",
"info": {
"title": "Error with Secrets",
"description": "Tests whether loggers/tracers redact secrets and PII within error responses.",
"version": "1.0.0"
},
"host": "localhost:3000",
"schemes": ["http"],
"produces": ["application/json"],
"securityDefinitions": {
"SharedKey": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
},
"paths": {
"/secrets/:create": {
"post": {
"operationId": "CreateSecret",
"description": "Creates a secret.",
"security": [
{
"SharedKey": []
}
],
"responses": {
"200": {
"description": "This method will always return an error.",
"schema": {
"$ref": "#/definitions/SecretResponse"
}
},
"default": {
"description": "An error occurred.",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
}
}
}
},
"/secrets/error": {
"get": {
"operationId": "GetErrorWithSecrets",
"description": "Gets an error response containing secrets and PII.",
"responses": {
"204": {
"description": "This method will always return an error."
},
"default": {
"description": "An error occurred.",
"schema": {
"$ref": "#/definitions/ErrorResponse"
}
}
}
}
}
},
"definitions": {
"SecretResponse": {
"type": "object",
"description": "A secret.",
"required": ["key", "value"],
"properties": {
"key": {
"type": "string",
"description": "The secret key."
},
"value": {
"type": "string",
"description": "The secret value."
}
}
},
"ErrorResponse": {
"type": "object",
"description": "Error response.",
"additionalProperties": true,
"required": ["error"],
"properties": {
"error": {
"description": "The error object.",
"$ref": "#/definitions/Error"
}
}
},
"Error": {
"type": "object",
"description": "The error object.",
"additionalProperties": true,
"required": ["code", "message"],
"properties": {
"code": {
"description": "One of a server-defined set of error codes.",
"$ref": "#/definitions/ErrorCode"
},
"message": {
"type": "string",
"description": "A human-readable representation of the error."
},
"target": {
"type": "string",
"description": "The target of the error."
},
"details": {
"type": "array",
"description": "An array of details about specific errors that led to this reported error.",
"items": {
"$ref": "#/definitions/Error"
}
},
"innererror": {
"description": "An object containing more specific information than the current object about the error.",
"$ref": "#/definitions/InnerError"
}
}
},
"InnerError": {
"type": "object",
"description": "An object containing more specific information about the error. As per Microsoft One API guidelines - https://github.com/Microsoft/api-guidelines/blob/vNext/Guidelines.md#7102-error-condition-responses.",
"additionalProperties": true,
"required": ["code", "message"],
"properties": {
"code": {
"description": "One of a server-defined set of error codes.",
"$ref": "#/definitions/InnerErrorCode"
},
"message": {
"type": "string",
"description": "Error message."
},
"innererror": {
"description": "An object containing more specific information than the current object about the error.",
"$ref": "#/definitions/InnerError"
}
}
},
"ErrorCode": {
"type": "string",
"description": "Human-readable error code.",
"x-ms-enum": {
"name": "ErrorCode",
"modelAsString": true
},
"enum": ["BadParameter", "Unauthorized"]
},
"InnerErrorCode": {
"type": "string",
"description": "Human-readable error code.",
"x-ms-enum": {
"name": "InnerErrorCode",
"modelAsString": true
},
"enum": ["MissingSharedKey", "UnauthorizedSharedKey"]
}
}
}