From 812d4184c5c3572403574d4afada609628cd83e3 Mon Sep 17 00:00:00 2001 From: Jeffrey Yasskin Date: Wed, 31 Jan 2024 06:35:58 -0800 Subject: [PATCH] Make refreshing the XSRF token conditional on whether the openapi endpoint needs authentication. (#3626) * Baseline openapi bindings. * Make refreshing the XSRF token conditional on whether the openapi endpoint needs authentication. * Update the test for the new middleware behavior. --- client-src/js-src/openapi-client.js | 12 +++++++----- client-src/js-src/openapi-client_test.js | 17 +++++++++++------ gen/js/chromestatus-openapi/package.json | 2 +- .../chromestatus-openapi/src/apis/DefaultApi.ts | 12 ++++++++++++ .../controllers/security_controller.py | 16 ++++++++++++++++ .../chromestatus_openapi/openapi/openapi.yaml | 12 ++++++++++++ .../test/test_default_controller.py | 3 +++ gen/py/chromestatus_openapi/requirements.txt | 4 ++-- openapi/api.yaml | 11 +++++++++++ package-lock.json | 7 ++++--- 10 files changed, 79 insertions(+), 17 deletions(-) diff --git a/client-src/js-src/openapi-client.js b/client-src/js-src/openapi-client.js index 24235125818..723b711a9d2 100644 --- a/client-src/js-src/openapi-client.js +++ b/client-src/js-src/openapi-client.js @@ -28,6 +28,7 @@ class ChromeStatusOpenApiClient extends Api { constructor() { super(new Configuration({ credentials: 'same-origin', + apiKey: 'placeholder-api-key', middleware: [ {pre: ChromeStatusMiddlewares.xsrfMiddleware}, {post: ChromeStatusMiddlewares.xssiMiddleware}, @@ -45,12 +46,13 @@ class ChromeStatusMiddlewares { * @return {Promise} */ static async xsrfMiddleware(req) { - return window.csClient.ensureTokenIsValid().then(() => { - const headers = req.init.headers || {}; - headers['X-Xsrf-Token'] = [window.csClient.token]; + const headers = req.init.headers || {}; + if ('X-Xsrf-Token' in headers) { + await window.csClient.ensureTokenIsValid(); + headers['X-Xsrf-Token'] = window.csClient.token; req.init.headers = headers; - return req; - }); + } + return req; } diff --git a/client-src/js-src/openapi-client_test.js b/client-src/js-src/openapi-client_test.js index b0cbf13c7a0..0badf82d8d3 100644 --- a/client-src/js-src/openapi-client_test.js +++ b/client-src/js-src/openapi-client_test.js @@ -30,29 +30,34 @@ describe('openapi-client', () => { }); describe('Middlewares', () => { describe('xsrfMiddleware', () => { - it('should add the XSRF token to the request with existing headers', async () => { + it('should update the XSRF token to the request with existing headers', async () => { const tokenValidStub = sinon.stub(window.csClient, 'ensureTokenIsValid').resolves(); /** @type {import('chromestatus-openapi').RequestContext} */ const req = { init: { - headers: {'content-type': ['application/json']}, + headers: { + 'content-type': ['application/json'], + 'X-Xsrf-Token': 'Wrong-value', + }, }, }; /** @type {import('chromestatus-openapi').FetchParams} */ const params = await ChromeStatusMiddlewares.xsrfMiddleware(req); assert.equal(params.init.headers['content-type'][0], 'application/json'); - assert.equal(params.init.headers['X-Xsrf-Token'][0], 'fake_token'); + assert.equal(params.init.headers['X-Xsrf-Token'], 'fake_token'); tokenValidStub.restore(); }); - it('should add the XSRF token to the request with no existing headers', async () => { + it('should not add the XSRF token to the request with no existing header', async () => { const tokenValidStub = sinon.stub(window.csClient, 'ensureTokenIsValid').resolves(); /** @type {import('chromestatus-openapi').RequestContext} */ const req = { - init: {}, + init: { + headers: {'content-type': ['application/json']}, + }, }; /** @type {import('chromestatus-openapi').FetchParams} */ const params = await ChromeStatusMiddlewares.xsrfMiddleware(req); - assert.equal(params.init.headers['X-Xsrf-Token'][0], 'fake_token'); + assert.notExists(params.init.headers['X-Xsrf-Token']); tokenValidStub.restore(); }); }); diff --git a/gen/js/chromestatus-openapi/package.json b/gen/js/chromestatus-openapi/package.json index 8393c50aae4..cc51bfb01fb 100644 --- a/gen/js/chromestatus-openapi/package.json +++ b/gen/js/chromestatus-openapi/package.json @@ -16,6 +16,6 @@ "prepare": "npm run build" }, "devDependencies": { - "typescript": "^5.3" + "typescript": "^4.0" } } diff --git a/gen/js/chromestatus-openapi/src/apis/DefaultApi.ts b/gen/js/chromestatus-openapi/src/apis/DefaultApi.ts index a032d437a63..48044d7c225 100644 --- a/gen/js/chromestatus-openapi/src/apis/DefaultApi.ts +++ b/gen/js/chromestatus-openapi/src/apis/DefaultApi.ts @@ -117,6 +117,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { headerParameters['Content-Type'] = 'application/json'; + if (this.configuration && this.configuration.apiKey) { + headerParameters["X-Xsrf-Token"] = this.configuration.apiKey("X-Xsrf-Token"); // XsrfToken authentication + } + const response = await this.request({ path: `/components/{componentId}/users/{userId}`.replace(`{${"componentId"}}`, encodeURIComponent(String(requestParameters.componentId))).replace(`{${"userId"}}`, encodeURIComponent(String(requestParameters.userId))), method: 'PUT', @@ -143,6 +147,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { const headerParameters: runtime.HTTPHeaders = {}; + if (this.configuration && this.configuration.apiKey) { + headerParameters["X-Xsrf-Token"] = this.configuration.apiKey("X-Xsrf-Token"); // XsrfToken authentication + } + const response = await this.request({ path: `/componentsusers`, method: 'GET', @@ -179,6 +187,10 @@ export class DefaultApi extends runtime.BaseAPI implements DefaultApiInterface { headerParameters['Content-Type'] = 'application/json'; + if (this.configuration && this.configuration.apiKey) { + headerParameters["X-Xsrf-Token"] = this.configuration.apiKey("X-Xsrf-Token"); // XsrfToken authentication + } + const response = await this.request({ path: `/components/{componentId}/users/{userId}`.replace(`{${"componentId"}}`, encodeURIComponent(String(requestParameters.componentId))).replace(`{${"userId"}}`, encodeURIComponent(String(requestParameters.userId))), method: 'DELETE', diff --git a/gen/py/chromestatus_openapi/chromestatus_openapi/controllers/security_controller.py b/gen/py/chromestatus_openapi/chromestatus_openapi/controllers/security_controller.py index 6d294ffd6df..fddbb48276f 100644 --- a/gen/py/chromestatus_openapi/chromestatus_openapi/controllers/security_controller.py +++ b/gen/py/chromestatus_openapi/chromestatus_openapi/controllers/security_controller.py @@ -1,2 +1,18 @@ from typing import List + +def info_from_XsrfToken(api_key, required_scopes): + """ + Check and retrieve authentication information from api_key. + Returned value will be passed in 'token_info' parameter of your operation function, if there is one. + 'sub' or 'uid' will be set in 'user' parameter of your operation function, if there is one. + + :param api_key API key provided by Authorization header + :type api_key: str + :param required_scopes Always None. Used for other authentication method + :type required_scopes: None + :return: Information attached to provided api_key or None if api_key is invalid or does not allow access to called API + :rtype: dict | None + """ + return {'uid': 'user_id'} + diff --git a/gen/py/chromestatus_openapi/chromestatus_openapi/openapi/openapi.yaml b/gen/py/chromestatus_openapi/chromestatus_openapi/openapi/openapi.yaml index a4d4f31a89e..e7bccaa4fb8 100644 --- a/gen/py/chromestatus_openapi/chromestatus_openapi/openapi/openapi.yaml +++ b/gen/py/chromestatus_openapi/chromestatus_openapi/openapi/openapi.yaml @@ -41,6 +41,8 @@ paths: responses: "200": description: Success + security: + - XsrfToken: [] summary: Remove a user from a component x-openapi-router-controller: chromestatus_openapi.controllers.default_controller put: @@ -70,6 +72,8 @@ paths: responses: "200": description: Success + security: + - XsrfToken: [] summary: Add a user to a component x-openapi-router-controller: chromestatus_openapi.controllers.default_controller /componentsusers: @@ -83,6 +87,8 @@ paths: $ref: '#/components/schemas/ComponentsUsersResponse' description: List of all the potential users and components with existing subscribers and owners. + security: + - XsrfToken: [] summary: List all components and possible users x-openapi-router-controller: chromestatus_openapi.controllers.default_controller components: @@ -189,3 +195,9 @@ components: title: owner type: boolean title: ComponentUsersRequest + securitySchemes: + XsrfToken: + in: header + name: X-Xsrf-Token + type: apiKey + x-apikeyInfoFunc: chromestatus_openapi.controllers.security_controller.info_from_XsrfToken diff --git a/gen/py/chromestatus_openapi/chromestatus_openapi/test/test_default_controller.py b/gen/py/chromestatus_openapi/chromestatus_openapi/test/test_default_controller.py index 259bc3cf00f..6a49dd5fabe 100644 --- a/gen/py/chromestatus_openapi/chromestatus_openapi/test/test_default_controller.py +++ b/gen/py/chromestatus_openapi/chromestatus_openapi/test/test_default_controller.py @@ -18,6 +18,7 @@ def test_add_user_to_component(self): component_users_request = {"owner":True} headers = { 'Content-Type': 'application/json', + 'XsrfToken': 'special-key', } response = self.client.open( '/api/v0/components/{component_id}/users/{user_id}'.format(component_id=56, user_id=56), @@ -35,6 +36,7 @@ def test_list_component_users(self): """ headers = { 'Accept': 'application/json', + 'XsrfToken': 'special-key', } response = self.client.open( '/api/v0/componentsusers', @@ -51,6 +53,7 @@ def test_remove_user_from_component(self): component_users_request = {"owner":True} headers = { 'Content-Type': 'application/json', + 'XsrfToken': 'special-key', } response = self.client.open( '/api/v0/components/{component_id}/users/{user_id}'.format(component_id=56, user_id=56), diff --git a/gen/py/chromestatus_openapi/requirements.txt b/gen/py/chromestatus_openapi/requirements.txt index 515a9e7bbb7..be4c8a3a02c 100644 --- a/gen/py/chromestatus_openapi/requirements.txt +++ b/gen/py/chromestatus_openapi/requirements.txt @@ -4,8 +4,8 @@ connexion[swagger-ui] <= 2.3.0; python_version=="3.5" or python_version=="3.4" # connexion requires werkzeug but connexion < 2.4.0 does not install werkzeug # we must peg werkzeug versions below to fix connexion # https://github.com/zalando/connexion/pull/1044 -werkzeug == 3.0.1; python_version=="3.5" or python_version=="3.4" +werkzeug == 0.16.1; python_version=="3.5" or python_version=="3.4" swagger-ui-bundle >= 0.0.2 python_dateutil >= 2.6.0 setuptools >= 21.0.0 -Flask == 2.3.2 +Flask == 2.1.1 diff --git a/openapi/api.yaml b/openapi/api.yaml index d76021fab80..4081da38f69 100644 --- a/openapi/api.yaml +++ b/openapi/api.yaml @@ -19,6 +19,8 @@ paths: get: summary: List all components and possible users operationId: listComponentUsers + security: + - XsrfToken: [] responses: '200': description: List of all the potential users and components with existing subscribers and owners. @@ -30,6 +32,8 @@ paths: put: summary: Add a user to a component operationId: addUserToComponent + security: + - XsrfToken: [] parameters: - in: path name: componentId @@ -54,6 +58,8 @@ paths: delete: summary: Remove a user from a component operationId: removeUserFromComponent + security: + - XsrfToken: [] parameters: - in: path name: componentId @@ -76,6 +82,11 @@ paths: schema: $ref: '#/components/schemas/ComponentUsersRequest' components: + securitySchemes: + XsrfToken: + type: apiKey + in: header + name: X-Xsrf-Token schemas: ComponentsUsersResponse: properties: diff --git a/package-lock.json b/package-lock.json index 0d3a02561d1..9e9d02ad77e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,7 +74,7 @@ "gen/js/chromestatus-openapi": { "version": "1.0.0", "devDependencies": { - "typescript": "^5.3" + "typescript": "^4.0" } }, "gen/js/chromestatus-openapi/node_modules/typescript": { @@ -19655,11 +19655,12 @@ "chromestatus-openapi": { "version": "file:gen/js/chromestatus-openapi", "requires": { - "typescript": "^5.3" + "typescript": "^4.0" }, "dependencies": { "typescript": { - "version": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true }